Merge pull request 'development' (#1) from development into main
Some checks failed
armco-org/utils/pipeline/head There was a failure building this commit
Some checks failed
armco-org/utils/pipeline/head There was a failure building this commit
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,3 +1,17 @@
|
||||
dist
|
||||
node_modules
|
||||
build
|
||||
build
|
||||
HOC
|
||||
adapters
|
||||
chartGenerators
|
||||
contexts
|
||||
dateHelper
|
||||
dateformat
|
||||
domHelper
|
||||
gridHelper
|
||||
helper
|
||||
hooks
|
||||
network
|
||||
providers
|
||||
recursionHelper
|
||||
validators
|
||||
6
Jenkinsfile
vendored
Normal file
6
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
@Library('jenkins-shared') _
|
||||
kanikoPipeline(
|
||||
repoName: 'utils',
|
||||
branch: env.BRANCH_NAME ?: 'main',
|
||||
isNpmLib: true
|
||||
)
|
||||
@@ -3,10 +3,34 @@
|
||||
# Get the directory of the current script
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
rm -rf build
|
||||
npx tsc
|
||||
vite build
|
||||
# Default values
|
||||
DEV_FLAG=""
|
||||
|
||||
# Parse arguments
|
||||
for arg in "$@"
|
||||
do
|
||||
case $arg in
|
||||
--dev)
|
||||
DEV_FLAG="--dev"
|
||||
shift # Remove --dev from processing
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "[BUILD:SH] Dev flag is: $DEV_FLAG"
|
||||
echo "[BUILD:SH] Removing build if exists"
|
||||
rm -rf build
|
||||
echo "[BUILD:SH] Checking TS Types"
|
||||
npx tsc
|
||||
echo "[BUILD:SH] Initiating build..."
|
||||
# Conditionally use vite-dev.config.ts if --dev flag is present
|
||||
if [ "$DEV_FLAG" == "--dev" ]; then
|
||||
vite build --config vite-dev.config.ts
|
||||
else
|
||||
vite build
|
||||
fi
|
||||
|
||||
echo "[BUILD:SH] Running post processor scripts..."
|
||||
# Run Post processors: Update style imports in .js files, create component modules
|
||||
node "$SCRIPT_DIR/post-processor.js" build/cjs
|
||||
node "$SCRIPT_DIR/post-processor.js" build/es
|
||||
node "$SCRIPT_DIR/post-processor.js" build/cjs $DEV_FLAG
|
||||
node "$SCRIPT_DIR/post-processor.js" build/es $DEV_FLAG
|
||||
@@ -7,17 +7,17 @@ const __dirname = dirname(__filename)
|
||||
|
||||
const exclusions = ["enums.js", "v4.js", "index.js"]
|
||||
|
||||
async function generateModule(fileName) {
|
||||
if (!exclusions.includes(fileName)) {
|
||||
async function generateModule(fileName, isDev) {
|
||||
if (!exclusions.includes(fileName) && !fileName.endsWith(".js.map")) {
|
||||
const dir = fileName.slice(0, -3)
|
||||
const name = `@armco/utils/${dir}`
|
||||
const packageJsonContent = {
|
||||
name,
|
||||
main: `../cjs/${dir}.js`,
|
||||
module: `../es/${dir}.js`,
|
||||
types: `../types/${dir}.d.ts`,
|
||||
main: `../${isDev ? "build/" : ""}cjs/${dir}.js`,
|
||||
module: `../${isDev ? "build/" : ""}es/${dir}.js`,
|
||||
types: `../${isDev ? "build/" : ""}types/${dir}.d.ts`,
|
||||
}
|
||||
const dirPath = resolve(__dirname, `../build/${dir}`)
|
||||
const dirPath = resolve(__dirname, `../${isDev ? "" : "build/"}${dir}`)
|
||||
try {
|
||||
await fs.mkdir(dirPath, { recursive: true })
|
||||
await fs.writeFile(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { readdir } from "fs/promises"
|
||||
import generateModule from "./generate-module.js"
|
||||
|
||||
async function postProcessor(dir) {
|
||||
async function postProcessor(dir, isDev) {
|
||||
try {
|
||||
const files = await readdir(dir)
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
await generateModule(file, dir)
|
||||
await generateModule(file, isDev)
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
@@ -15,8 +15,10 @@ async function postProcessor(dir) {
|
||||
}
|
||||
|
||||
const targetDir = process.argv[2]
|
||||
const isDev = process.argv.includes("--dev")
|
||||
|
||||
if (targetDir) {
|
||||
postProcessor(targetDir)
|
||||
postProcessor(targetDir, isDev)
|
||||
} else {
|
||||
console.error("Please provide the build directory to run post processor on.")
|
||||
process.exit(1)
|
||||
|
||||
2318
package-lock.json
generated
2318
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -1,36 +1,37 @@
|
||||
{
|
||||
"name": "@armco/utils",
|
||||
"version": "0.0.18",
|
||||
"version": "0.0.29",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "./build-tools/build.sh",
|
||||
"build:sm": "./build-tools/build.sh --dev",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint .",
|
||||
"publish:sh": "./publish.sh",
|
||||
"publish:local": "./publish-local.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/react": "^18.3.7",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"glob": "^11.0.0",
|
||||
"react": "^18.3.1",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-dts": "^4.2.1",
|
||||
"vite-plugin-externalize-deps": "^0.8.0",
|
||||
"vitest": "^2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@armco/configs": "^0.0.7",
|
||||
"@armco/types": "^0.0.9",
|
||||
"d3": "^7.9.0",
|
||||
"uuid": "^10.0.0"
|
||||
"@armco/configs": "^0.0.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">16.8.1"
|
||||
"@armco/types": "^0.0.18",
|
||||
"d3": "^7.9.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.18.2",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@armco/types": "^0.0.21",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"glob": "^11.0.3",
|
||||
"react-dom": "18.3.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-plugin-externalize-deps": "^0.10.0",
|
||||
"vitest": "^4.0.8"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"plugins": [
|
||||
@@ -46,7 +47,7 @@
|
||||
"types": "build/types/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ReStruct-Corporate-Advantage/utils.git"
|
||||
"url": "git+https://gitea.armco.dev/ReStruct-Corporate-Advantage/utils.git"
|
||||
},
|
||||
"files": [
|
||||
"build"
|
||||
@@ -59,7 +60,7 @@
|
||||
],
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ReStruct-Corporate-Advantage/utils/issues"
|
||||
"url": "https://gitea.armco.dev/ReStruct-Corporate-Advantage/utils/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ReStruct-Corporate-Advantage/utils#readme"
|
||||
"homepage": "https://gitea.armco.dev/ReStruct-Corporate-Advantage/utils#readme"
|
||||
}
|
||||
|
||||
20
publish.sh
20
publish.sh
@@ -6,11 +6,23 @@ set -e
|
||||
npm --no-git-tag-version version ${semver}
|
||||
npm run build
|
||||
cp package.json build/
|
||||
sed -i '' -E 's/"build"/"*"/' build/package.json
|
||||
|
||||
sed -i '' 's#"build/cjs/index.js"#"cjs/index.js"#' build/package.json
|
||||
sed -i '' 's#"build/es/index.js"#"es/index.js"#' build/package.json
|
||||
sed -i '' 's#"build/types/index.d.ts"#"types/index.d.ts"#' build/package.json
|
||||
# Use Node.js for portable package.json normalization
|
||||
# Pass the target path via env var to avoid Node treating it as a module/script argument
|
||||
PKG_PATH="$(pwd)/build/package.json" node - <<'EOF'
|
||||
const fs = require('fs');
|
||||
const path = process.env.PKG_PATH;
|
||||
const pkg = JSON.parse(fs.readFileSync(path, 'utf8'));
|
||||
pkg.private = false;
|
||||
delete pkg.scripts;
|
||||
delete pkg.devDependencies;
|
||||
if (!pkg.files) pkg.files = ['*'];
|
||||
else pkg.files = pkg.files.map(x => x === 'build' ? '*' : x);
|
||||
['main','module','types'].forEach(k => {
|
||||
if (pkg[k]) pkg[k] = pkg[k].replace(/^build\//, '');
|
||||
});
|
||||
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n');
|
||||
EOF
|
||||
|
||||
cd build
|
||||
npm publish --access public --loglevel verbose
|
||||
|
||||
6
react-app-env.d/package.json
Normal file
6
react-app-env.d/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@armco/utils/react-app-env.d",
|
||||
"main": "../build/cjs/react-app-env.d.js",
|
||||
"module": "../build/es/react-app-env.d.js",
|
||||
"types": "../build/types/react-app-env.d.d.ts"
|
||||
}
|
||||
124
src/adapters.ts
124
src/adapters.ts
@@ -1,124 +0,0 @@
|
||||
import {
|
||||
ComponentDescription,
|
||||
ProgressiveChartData,
|
||||
ThreeDChartDataArrayFormat,
|
||||
ThreeDChartDataObjectFormat,
|
||||
TreeListData,
|
||||
RecusionConditionTypes,
|
||||
} from "@armco/types"
|
||||
import { injectIds } 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,
|
||||
]
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export function 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)
|
||||
})
|
||||
injectIds({
|
||||
data: returnTreeList,
|
||||
condition: { type: RecusionConditionTypes.KEY_EXISTS, key: "label" },
|
||||
iterateOn: "children",
|
||||
})
|
||||
return returnTreeList
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,5 @@ export const ArContext = createContext<ArContextType>({
|
||||
setRightPanelContent: () => {},
|
||||
setTheme: () => {},
|
||||
setUser: () => {},
|
||||
clearUser: () => {},
|
||||
})
|
||||
|
||||
@@ -1,546 +0,0 @@
|
||||
import { CalendarDate, Event, ArDateFormats } from "@armco/types"
|
||||
import { MONTH_INDEX } from "@armco/configs/constants"
|
||||
import { pad } from "./helper"
|
||||
|
||||
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 = pad(date.day, separator)
|
||||
const month = 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 }
|
||||
@@ -1,337 +0,0 @@
|
||||
import { ArDateMasks, FunctionType } 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")
|
||||
}
|
||||
|
||||
export { dateFormat as DateFormatter }
|
||||
440
src/domHelper.ts
440
src/domHelper.ts
@@ -1,440 +1,3 @@
|
||||
import { Children } from "react"
|
||||
import {
|
||||
CarouselProps,
|
||||
IconProps,
|
||||
ArPopoverPositions,
|
||||
ObjectType,
|
||||
Position,
|
||||
} from "@armco/types"
|
||||
import { getStylesFromClass } from "./helper"
|
||||
|
||||
const style = window.getComputedStyle(document.documentElement)
|
||||
const fontSize = parseFloat(style.fontSize)
|
||||
const markerSize = 7
|
||||
|
||||
export function outerWidth(el: HTMLElement) {
|
||||
let width = el.offsetWidth
|
||||
const style = getComputedStyle(el)
|
||||
|
||||
width += parseInt(style.marginLeft) + parseInt(style.marginRight)
|
||||
return width
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function hexToHsl(color: string, returnRaw?: boolean) {
|
||||
const [r, g, b] = hexToRgb(color)
|
||||
return rgbToHsl(r, g, b, returnRaw)
|
||||
}
|
||||
|
||||
export function 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
|
||||
*/
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function 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]
|
||||
}
|
||||
|
||||
export function 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)
|
||||
}
|
||||
|
||||
export function hslToHex(h: number, s: number, l: number) {
|
||||
const [r, g, b] = hslToRgb(h, s, l)
|
||||
return rgbToHex(Math.round(r), Math.round(g), Math.round(b))
|
||||
}
|
||||
|
||||
export function 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 = 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
|
||||
*/
|
||||
export function 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
|
||||
*/
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function isKeyboardEvent(
|
||||
e?: React.MouseEvent | React.KeyboardEvent,
|
||||
): e is React.KeyboardEvent {
|
||||
return e ? e.hasOwnProperty("key") : false
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function shouldUsePosition(
|
||||
position: ArPopoverPositions,
|
||||
anchorRect: DOMRect,
|
||||
popoverRect: DOMRect,
|
||||
demo?: boolean,
|
||||
) {
|
||||
const windowObj = 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 (
|
||||
fontSize > anchorRect.top ||
|
||||
fontSize > anchorRect.bottom
|
||||
)
|
||||
return false
|
||||
} else {
|
||||
if (
|
||||
fontSize > anchorRect.left ||
|
||||
fontSize > anchorRect.right
|
||||
)
|
||||
return false
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
||||
export function 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 : 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 : markerSize),
|
||||
},
|
||||
[ArPopoverPositions.LEFT]: {
|
||||
top: anchorRect.top + (anchorRect.height - popoverRect.height) / 2,
|
||||
left:
|
||||
anchorRect.left -
|
||||
popoverRect.width -
|
||||
(hideMarker ? 0 : markerSize),
|
||||
},
|
||||
[ArPopoverPositions.TOP]: {
|
||||
top:
|
||||
anchorRect.top -
|
||||
popoverRect.height -
|
||||
(hideMarker ? 0 : 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 = fontSize
|
||||
}
|
||||
if ((left as number) + popoverRect.width > window.innerWidth) {
|
||||
left = `calc(100% - ${popoverRect.width}px - ${fontSize}px)`
|
||||
}
|
||||
} else {
|
||||
if ((top as number) < 0) {
|
||||
top = fontSize
|
||||
}
|
||||
if ((top as number) + popoverRect.height > window.innerHeight) {
|
||||
top = `calc(100% - ${popoverRect.height}px - ${fontSize}px)`
|
||||
}
|
||||
}
|
||||
return { left, top }
|
||||
}
|
||||
|
||||
export function 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 (
|
||||
shouldUsePosition(position, anchorRect, popoverRect, demo)
|
||||
) {
|
||||
return position
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return ArPopoverPositions.BOTTOM
|
||||
}
|
||||
|
||||
export function calculatePopoverPosition(
|
||||
anchorRect: DOMRect,
|
||||
popoverRect: DOMRect,
|
||||
requestedPosition?: ArPopoverPositions,
|
||||
hideMarker?: boolean,
|
||||
clickCoordinates?: Array<number> | null,
|
||||
demo?: boolean,
|
||||
topOffset?: number,
|
||||
) {
|
||||
const windowObj = 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 = getPositionToUse(
|
||||
anchorRect,
|
||||
popoverRect,
|
||||
requestedPosition,
|
||||
demo,
|
||||
)
|
||||
const { left, top } = adjustPosition(
|
||||
position,
|
||||
anchorRect,
|
||||
popoverRect,
|
||||
hideMarker,
|
||||
topOffset,
|
||||
)
|
||||
return {
|
||||
leftTop: {
|
||||
top: ("" + top).startsWith("calc") ? top : top + "px",
|
||||
left: ("" + left).startsWith("calc") ? left : left + "px",
|
||||
},
|
||||
position,
|
||||
}
|
||||
}
|
||||
|
||||
export function openInNewTab(url: string) {
|
||||
const win = window.open(url, "_blank")
|
||||
if (win != null) {
|
||||
@@ -444,7 +7,8 @@ export function openInNewTab(url: string) {
|
||||
|
||||
export function download(data: string | Blob, filename?: string) {
|
||||
const anchor = document.createElement("a")
|
||||
anchor.href = typeof data === "string" ? data : window.URL.createObjectURL(data)
|
||||
anchor.href =
|
||||
typeof data === "string" ? data : window.URL.createObjectURL(data)
|
||||
anchor.download = filename || "edited-image.png"
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
|
||||
17
src/enums.ts
Normal file
17
src/enums.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export enum ArThemes {
|
||||
LIGHT1 = "th-light-1",
|
||||
DARK1 = "th-dark-1",
|
||||
}
|
||||
|
||||
export enum ArAlertType {
|
||||
SUCCESS = "success",
|
||||
WARNING = "warning",
|
||||
ERROR = "error",
|
||||
INFORMATION = "information",
|
||||
}
|
||||
|
||||
|
||||
export enum RecusionConditionTypes {
|
||||
KEY_EXISTS = "keyExists",
|
||||
KEY_VALUE = "keyValue",
|
||||
}
|
||||
@@ -1,733 +0,0 @@
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { FunctionType, GridToolbarSpecs, SlotDescriptor } from "@armco/types"
|
||||
import { getDocumentElement } from "./domHelper"
|
||||
|
||||
// For x and y toolbars only
|
||||
export function 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 = 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 = 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: generateGridTemplate(
|
||||
rowToolsGridArea,
|
||||
rowHeight || "minmax(auto, 1fr)",
|
||||
colWidth || "1fr",
|
||||
),
|
||||
},
|
||||
colTools: {
|
||||
slots: colSlots,
|
||||
gridTemplate: generateGridTemplate(
|
||||
[colToolsGridArea],
|
||||
"1fr",
|
||||
"1fr",
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function 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 = 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
|
||||
}
|
||||
|
||||
export function 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 }
|
||||
}
|
||||
|
||||
export function 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()
|
||||
}
|
||||
|
||||
export function 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)
|
||||
}
|
||||
}
|
||||
|
||||
export function 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)
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
export function 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,
|
||||
}
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function 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"
|
||||
] = isSlotSelected(slot, rowSlots, colSlots, selectForDelete)
|
||||
})
|
||||
return [...slots]
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function findIntersectingSlots(
|
||||
slots: Array<SlotDescriptor>,
|
||||
toolSlot: SlotDescriptor,
|
||||
isRow: boolean,
|
||||
) {
|
||||
return slots.filter((slot) => isIntersecting(toolSlot, slot, isRow))
|
||||
}
|
||||
|
||||
export function 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]
|
||||
}
|
||||
|
||||
export function 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> = 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 = findIntersectingSlots(
|
||||
slots,
|
||||
selectedToolSlot,
|
||||
isRow,
|
||||
)
|
||||
const newSlotsAtCurrentDimension = referenceSlots
|
||||
.map((referenceSlot) => {
|
||||
const lateralRefToolIntersectedSlot = intersectedSlots.find((s) =>
|
||||
isIntersecting(referenceSlot, s, !isRow),
|
||||
)
|
||||
if (lateralRefToolIntersectedSlot) {
|
||||
const shouldCreate = 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)
|
||||
}
|
||||
|
||||
export function 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 = generateInsertionIndexesForDimension(
|
||||
gridToolbarSpecs,
|
||||
"rowTools",
|
||||
"row",
|
||||
placement,
|
||||
isRow,
|
||||
)
|
||||
} else {
|
||||
insertAt = 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
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function 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 = generateSlots(Object.keys(rowsAndHeights))
|
||||
|
||||
return { slotConfigs, rowHeights, colWidths: colWidths || [] }
|
||||
}
|
||||
452
src/helper.tsx
452
src/helper.tsx
@@ -1,57 +1,27 @@
|
||||
import { lazy } from "react"
|
||||
import { JSX, 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
|
||||
export interface SearchArgs {
|
||||
obj: ObjectType | Array<ObjectType>
|
||||
key: string
|
||||
value?: string
|
||||
parent?: ObjectType | Array<ObjectType>
|
||||
}
|
||||
|
||||
interface StringifyOnce {
|
||||
(obj: any, replacer?: ReplacerFunction | null, indent?: number): string
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pathOrComponentName
|
||||
* @param fallback
|
||||
* @returns Lazy component, can be used as <LazyComponent {...props} />
|
||||
*/
|
||||
export function lazyImport(pathOrComponentName: string, fallback: FunctionType) {
|
||||
return lazy(() =>
|
||||
import(pathOrComponentName)
|
||||
.catch(() => import(`@armco/components/${pathOrComponentName}`))
|
||||
.catch(() => import(`@armco/shared-components/${pathOrComponentName}`))
|
||||
.catch(fallback),
|
||||
)
|
||||
}
|
||||
|
||||
export function populatePagesInRoutes(routes: RouteConfig[], fallback: FunctionType) {
|
||||
export function 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,
|
||||
),
|
||||
import(`../pages/${elementName}/${elementName}.tsx`).catch(fallback),
|
||||
)
|
||||
route.element = <Component />
|
||||
if (route.children) {
|
||||
@@ -85,6 +55,17 @@ export function populateComponentsInRoutes(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* WHAT: Recursively filters through an array of objects based on a filter string.
|
||||
* HOW: Checks specified keys in each object for a match with the filter string.
|
||||
* If an object has children, it recursively filters the children as well.
|
||||
* An object is included in the result if it or any of its children match the filter.
|
||||
* @param data
|
||||
* @param filter
|
||||
* @param matchCase
|
||||
* @param searchKeys
|
||||
* @returns
|
||||
*/
|
||||
export function recrusiveFilter(
|
||||
data: Array<any>,
|
||||
filter: string,
|
||||
@@ -107,16 +88,10 @@ export function recrusiveFilter(
|
||||
(matchCase
|
||||
? obj[key].indexOf(filter) > -1
|
||||
: obj[key] &&
|
||||
obj[key]
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.indexOf(filter.toLowerCase()) > -1)
|
||||
obj[key].toString().toLowerCase().indexOf(filter.toLowerCase()) >
|
||||
-1)
|
||||
if (obj.children) {
|
||||
obj.children = recrusiveFilter(
|
||||
obj.children,
|
||||
filter,
|
||||
matchCase,
|
||||
)
|
||||
obj.children = recrusiveFilter(obj.children, filter, matchCase)
|
||||
isMatch = isMatch || obj.children.length > 0
|
||||
}
|
||||
return acc || isMatch
|
||||
@@ -126,7 +101,19 @@ export function recrusiveFilter(
|
||||
return filteredItems
|
||||
}
|
||||
|
||||
export function search({ obj, key, value }: SearchArgs): ObjectType | undefined {
|
||||
/**
|
||||
* WHAT: Searches through a nested object or array to find an object with a specific key-value pair.
|
||||
* HOW: Recursively traverses the object/array, checking each object's keys and values.
|
||||
* If the specified key is found and its value matches (if provided), the object is returned.
|
||||
* If the object contains nested objects or arrays, the function calls itself on those nested structures.
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export function search({
|
||||
obj,
|
||||
key,
|
||||
value,
|
||||
}: SearchArgs): ObjectType | undefined {
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) {
|
||||
const result = search({ obj: item, key, value })
|
||||
@@ -146,119 +133,18 @@ export function search({ obj, key, value }: SearchArgs): ObjectType | undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function 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 = 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 = 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
|
||||
}
|
||||
}
|
||||
|
||||
export function generateSlices(
|
||||
count: number,
|
||||
sliceLength: number,
|
||||
labelFormat: string,
|
||||
/**
|
||||
* WHAT: Converts a string to camelCase format.
|
||||
* @param str
|
||||
* @param separater
|
||||
* @param includeFirst
|
||||
* @returns
|
||||
*/
|
||||
export function toCamelCase(
|
||||
str: string,
|
||||
separater: string,
|
||||
includeFirst: boolean,
|
||||
) {
|
||||
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
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function 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> } = aggregate(
|
||||
data,
|
||||
key,
|
||||
)
|
||||
groups[key] = group
|
||||
})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
export function toCamelCase(str: string, separater: string, includeFirst: boolean) {
|
||||
const strParts = str.split(separater || " ")
|
||||
return strParts
|
||||
.map((part, i) =>
|
||||
@@ -269,12 +155,11 @@ export function toCamelCase(str: string, separater: string, includeFirst: boolea
|
||||
.join("")
|
||||
}
|
||||
|
||||
export function generateRandomId(length: number = 8) {
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
.substring(2, length + 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* WHAT: Copies text to clipboard or prompts user to copy if clipboard access is not available.
|
||||
* @param text
|
||||
* @param cb
|
||||
*/
|
||||
export function copyOrPrompt(text?: string, cb?: FunctionType) {
|
||||
if (copyToClipboard(text)) {
|
||||
cb && cb()
|
||||
@@ -283,6 +168,11 @@ export function copyOrPrompt(text?: string, cb?: FunctionType) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WHAT: Copies text to clipboard if supported by the browser.
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
export function copyToClipboard(text?: string) {
|
||||
const isNotSafari = typeof (window as any).safari === "undefined"
|
||||
if (isNotSafari) {
|
||||
@@ -293,72 +183,6 @@ export function copyToClipboard(text?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function 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,
|
||||
}
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
|
||||
export function importComponent(
|
||||
name: string,
|
||||
hierarchy: string,
|
||||
fallback: FunctionType,
|
||||
) {
|
||||
return lazy(
|
||||
() =>
|
||||
import(`../components/${hierarchy}/${name}/${name}.tsx`).catch(
|
||||
fallback,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function getStylesFromClass(className: string): Record<string, string> {
|
||||
const styleSheet = Array.from(document.styleSheets).find((sheet) =>
|
||||
Array.from(sheet.cssRules).some(
|
||||
@@ -386,6 +210,14 @@ export function getStylesFromClass(className: string): Record<string, string> {
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* WHAT: Creates a debounced version of a function that delays its execution until after a specified wait time has elapsed since the last time it was invoked.
|
||||
* HOW: Uses a timeout to track the delay period. If the debounced function is called again before the timeout expires, the previous timeout is cleared and a new one is set.
|
||||
* @param func
|
||||
* @param wait
|
||||
* @param immediate
|
||||
* @returns
|
||||
*/
|
||||
export function debounce<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
wait?: number,
|
||||
@@ -412,118 +244,72 @@ export function debounce<T extends (...args: any[]) => void>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WHAT: Detects if the current device is a mobile device based on the user agent string.
|
||||
* HOW: Uses a regular expression to match common mobile device identifiers in the user agent string.
|
||||
* @returns
|
||||
*/
|
||||
export function 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),
|
||||
; (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,
|
||||
)
|
||||
check = true
|
||||
})(
|
||||
(navigator.userAgent ||
|
||||
navigator.vendor ||
|
||||
("opera" in window && window.opera)) as string,
|
||||
)
|
||||
return check
|
||||
}
|
||||
|
||||
export function getCookie(cookieName: string) {
|
||||
return (
|
||||
document.cookie
|
||||
.match("(^|;)\\s*" + cookieName + "\\s*=\\s*([^;]+)")
|
||||
?.pop() || ""
|
||||
)
|
||||
}
|
||||
|
||||
export function setCookie(cname: string, cvalue: string, exdays: number) {
|
||||
const d = new Date()
|
||||
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000)
|
||||
let expires = "expires=" + d.toUTCString()
|
||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";Path=/"
|
||||
}
|
||||
|
||||
export function clearCookie(cname: string) {
|
||||
document.cookie = cname + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads a number with leading zeros to ensure it has at least two digits.
|
||||
* Optionally appends a separator at the end.
|
||||
* @param num
|
||||
* @param separator
|
||||
* @returns
|
||||
*/
|
||||
export function pad(num: number, separator?: string) {
|
||||
return ("" + num).padStart(2, "0") + (separator ? separator : "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a string and converts it to a more readable format.
|
||||
* HOW: Splits camelCase words and capitalizes the first letter of each word.
|
||||
*
|
||||
* @param str Input string to be converted
|
||||
* @returns Converted string
|
||||
*/
|
||||
export function toReadable(str?: string) {
|
||||
return str
|
||||
?.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||
.split(" ")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
export function generateFileKey(file: File | { name: string }) {
|
||||
return "size" in file
|
||||
? `${file.name}_${file.size}_${file.type}_${file.lastModified}`
|
||||
: file.name + "-preexisting-" + Date.now()
|
||||
}
|
||||
|
||||
export function 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
|
||||
}
|
||||
}
|
||||
|
||||
export function isImage(type: string) {
|
||||
return !!type && validImageMimeTypes.indexOf(type) > -1
|
||||
}
|
||||
|
||||
export const 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)
|
||||
}
|
||||
}
|
||||
189
src/hooks.ts
189
src/hooks.ts
@@ -1,6 +1,9 @@
|
||||
import { useContext, useEffect, useState } from "react"
|
||||
import { useCallback, useContext, useEffect, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import {
|
||||
AlertProps,
|
||||
ArAlertType,
|
||||
ArThemes,
|
||||
DrawerProps,
|
||||
FunctionType,
|
||||
@@ -8,7 +11,11 @@ import {
|
||||
PanelContent,
|
||||
User,
|
||||
} from "@armco/types"
|
||||
import { SESSION_TOKEN_NAME } from "@armco/configs/constants"
|
||||
import { ENDPOINTS, IAM } from "@armco/configs/endpoints"
|
||||
import { ArContext, SlotterContext } from "./contexts"
|
||||
import { clearCookie, setCookie } from "./helper"
|
||||
import { get, post } from "./network"
|
||||
|
||||
export function useSlotted(componentName: string) {
|
||||
const { slotted, setSlotted } = useContext(SlotterContext)
|
||||
@@ -17,59 +24,6 @@ export function useSlotted(componentName: string) {
|
||||
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
|
||||
@@ -110,7 +64,7 @@ export const useNotification = (): {
|
||||
}
|
||||
|
||||
export const useDrawerState = (): {
|
||||
drawerState: DrawerProps
|
||||
drawerState?: DrawerProps
|
||||
setDrawerState: (drawerState: DrawerProps) => void
|
||||
} => {
|
||||
const context = useContext(ArContext)
|
||||
@@ -138,8 +92,9 @@ export const useLoggedIn = (): {
|
||||
}
|
||||
|
||||
export const useUser = (): {
|
||||
user?: User
|
||||
setUser: (user: User) => void
|
||||
user?: User | null
|
||||
setUser: (user: User | null) => void
|
||||
clearUser: () => void
|
||||
} => {
|
||||
const context = useContext(ArContext)
|
||||
if (!context) {
|
||||
@@ -148,6 +103,7 @@ export const useUser = (): {
|
||||
return {
|
||||
user: context.user,
|
||||
setUser: context.setUser,
|
||||
clearUser: context.clearUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,3 +124,122 @@ export const usePanelContent = (
|
||||
: context.setRightPanelContent,
|
||||
}
|
||||
}
|
||||
|
||||
export function useClearSession() {
|
||||
const { setLoggedIn } = useLoggedIn()
|
||||
const { clearUser } = useUser()
|
||||
|
||||
return () => {
|
||||
setLoggedIn(false)
|
||||
clearUser()
|
||||
clearCookie(SESSION_TOKEN_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const { setUser } = useUser()
|
||||
const { isLoggedIn, setLoggedIn } = useLoggedIn()
|
||||
const { notify } = useNotification()
|
||||
const clearSession = useClearSession()
|
||||
const navigate = useNavigate()
|
||||
const { setPanelContent: setRightPanelContent } = usePanelContent(false)
|
||||
|
||||
const postAuthSuccess = useCallback(
|
||||
(
|
||||
response: { status: number; body: any },
|
||||
shouldNotify?: boolean,
|
||||
shouldLoginParent?: boolean,
|
||||
) => {
|
||||
if (response && response.body && response.status === 200) {
|
||||
const user = response.body.user as User
|
||||
if (user?.username) {
|
||||
if (!user.name && user.firstName) {
|
||||
user.name = `${user.firstName}${
|
||||
user.lastName ? " " + user.lastName : ""
|
||||
}`
|
||||
}
|
||||
!isLoggedIn && setLoggedIn(true)
|
||||
setUser(user)
|
||||
setCookie("auth-session-user", user.username, 30)
|
||||
shouldNotify &&
|
||||
notify({
|
||||
show: true,
|
||||
message: "Logged in successfully!",
|
||||
type: ArAlertType.SUCCESS,
|
||||
uid: uuid(),
|
||||
})
|
||||
if (shouldLoginParent) {
|
||||
window.parent.postMessage(
|
||||
{ event: "LOGGED_IN", user: response.body },
|
||||
"*",
|
||||
)
|
||||
} else {
|
||||
setRightPanelContent({ componentName: "" })
|
||||
window.location.pathname === "/" && navigate(`/${user.username}`)
|
||||
}
|
||||
} else {
|
||||
notify({
|
||||
message: "Malformed user details, try again or contact support!",
|
||||
type: ArAlertType.ERROR,
|
||||
uid: uuid(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
clearSession()
|
||||
notify({
|
||||
message: "Unknown error occurred on fetching user details!",
|
||||
type: ArAlertType.ERROR,
|
||||
uid: uuid(),
|
||||
})
|
||||
}
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const postAuthFailure = useCallback(
|
||||
(
|
||||
error: { status: number },
|
||||
shouldNotify?: boolean,
|
||||
shouldLogoutParent?: boolean,
|
||||
) => {
|
||||
if (!error.status || error.status === 403) {
|
||||
shouldNotify &&
|
||||
notify({
|
||||
message: "Unable to fetch user details, logging out...",
|
||||
type: ArAlertType.ERROR,
|
||||
uid: uuid(),
|
||||
})
|
||||
console.error(
|
||||
"Attempt fetching user details at page load failed",
|
||||
error,
|
||||
)
|
||||
clearSession()
|
||||
if (shouldLogoutParent) {
|
||||
window.parent.postMessage({ event: "NOT_LOGGED_IN" }, "*")
|
||||
} else {
|
||||
setRightPanelContent({ componentName: "" })
|
||||
window.location.pathname !== "/" && navigate("/")
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
return {
|
||||
check: () => {
|
||||
return get(
|
||||
IAM[process.env.NODE_ENV] +
|
||||
ENDPOINTS.USERS.ROOT +
|
||||
ENDPOINTS.USERS.CHECK,
|
||||
)
|
||||
},
|
||||
postAuthSuccess,
|
||||
postAuthFailure,
|
||||
login: (credentials: { [key: string]: string | number | File }) => {
|
||||
return post(
|
||||
IAM[process.env.NODE_ENV] + ENDPOINTS.AUTH.ROOT + ENDPOINTS.AUTH.LOGIN,
|
||||
credentials,
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
export * from "./dateformat"
|
||||
export * from "./domHelper"
|
||||
export * from "./adapters"
|
||||
export * from "./helper"
|
||||
export * from "./network"
|
||||
export * from "./validators"
|
||||
export * from "./gridHelper"
|
||||
export * from "../../taskerclient/src/validators"
|
||||
export * from "./recursionHelper"
|
||||
export * from "./dateHelper"
|
||||
export * from "./chartGenerators"
|
||||
export * from "./hooks"
|
||||
export * from "./contexts"
|
||||
export * from "./providers"
|
||||
export * from "./HOC"
|
||||
export * from "./enums"
|
||||
|
||||
371
src/network.ts
371
src/network.ts
@@ -1,5 +1,7 @@
|
||||
import { FunctionType, ObjectType } from "@armco/types"
|
||||
import { HOST, STATIC_HOST } from "@armco/configs/endpoints"
|
||||
import { SESSION_TOKEN_NAME } from "@armco/configs/constants"
|
||||
import { getCookie } from "./helper"
|
||||
|
||||
interface Options extends RequestInit {
|
||||
// headers?: {
|
||||
@@ -36,210 +38,215 @@ class Error {
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_ATTEMPTS_LIMIT = 10
|
||||
const MAX_ATTEMPTS_LIMIT = 10
|
||||
|
||||
export async function get(url: string, queryParams?: object, options?: Options) {
|
||||
const urlString = stringifyUrl(url, queryParams)
|
||||
options = {
|
||||
method: "GET",
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
...options,
|
||||
}
|
||||
return crud(urlString, options)
|
||||
export async function get(
|
||||
url: string,
|
||||
queryParams?: object,
|
||||
options?: Options,
|
||||
) {
|
||||
const urlString = stringifyUrl(url, queryParams)
|
||||
options = {
|
||||
method: "GET",
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
...options,
|
||||
}
|
||||
return crud(urlString, options)
|
||||
}
|
||||
|
||||
export async function getStatic(
|
||||
url: string,
|
||||
queryParams?: object | null,
|
||||
options?: Options | null,
|
||||
) {
|
||||
let urlString = stringifyUrl(url, queryParams)
|
||||
const environment = process.env.NODE_ENV || "development"
|
||||
const host = urlString.startsWith("http")
|
||||
? ""
|
||||
: STATIC_HOST[environment as keyof object]
|
||||
urlString = host ? host + urlString : urlString
|
||||
options = {
|
||||
method: "GET",
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
...options,
|
||||
}
|
||||
return crud(urlString, options)
|
||||
export async function getStatic(
|
||||
url: string,
|
||||
queryParams?: object | null,
|
||||
options?: Options | null,
|
||||
) {
|
||||
let urlString = stringifyUrl(url, queryParams)
|
||||
const environment = process.env.NODE_ENV || "development"
|
||||
const host = urlString.startsWith("http")
|
||||
? ""
|
||||
: STATIC_HOST[environment as keyof object]
|
||||
urlString = host ? host + urlString : urlString
|
||||
options = {
|
||||
method: "GET",
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
...options,
|
||||
}
|
||||
return crud(urlString, options)
|
||||
}
|
||||
|
||||
export async function post(
|
||||
url: string,
|
||||
data: object,
|
||||
queryParams?: object,
|
||||
options?: Options,
|
||||
noStringify?: boolean,
|
||||
noHeaders?: boolean,
|
||||
) {
|
||||
const urlString = 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" })
|
||||
export async function post(
|
||||
url: string,
|
||||
data: object,
|
||||
queryParams?: object,
|
||||
options?: Options,
|
||||
noStringify?: boolean,
|
||||
noHeaders?: boolean,
|
||||
) {
|
||||
const urlString = stringifyUrl(url, queryParams)
|
||||
options = {
|
||||
method: "POST",
|
||||
// @ts-ignore
|
||||
return crud(urlString, options)
|
||||
body: noStringify ? data : JSON.stringify(data),
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
}
|
||||
!noHeaders &&
|
||||
options &&
|
||||
(options.headers = { "Content-Type": "application/json" })
|
||||
// @ts-ignore
|
||||
return crud(urlString, options)
|
||||
}
|
||||
|
||||
export async function upload(
|
||||
url: string,
|
||||
file: File,
|
||||
queryParams?: object,
|
||||
options?: Options,
|
||||
) {
|
||||
let urlString = stringifyUrl(url, queryParams)
|
||||
const environment = process.env.NODE_ENV || "development"
|
||||
const host = urlString.startsWith("http")
|
||||
? ""
|
||||
: 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 crud(urlString, options)
|
||||
export async function upload(
|
||||
url: string,
|
||||
file: File,
|
||||
queryParams?: object,
|
||||
options?: Options,
|
||||
) {
|
||||
let urlString = stringifyUrl(url, queryParams)
|
||||
const environment = process.env.NODE_ENV || "development"
|
||||
const host = urlString.startsWith("http")
|
||||
? ""
|
||||
: 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 crud(urlString, options)
|
||||
}
|
||||
|
||||
export async function put(url: string, data: object, queryParams?: object) {
|
||||
const urlString = stringifyUrl(url, queryParams)
|
||||
const options: Options = {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
return crud(urlString, options)
|
||||
export async function put(url: string, data: object, queryParams?: object) {
|
||||
const urlString = stringifyUrl(url, queryParams)
|
||||
const options: Options = {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
return crud(urlString, options)
|
||||
}
|
||||
|
||||
export async function deleteRec(url: string, queryParams: object) {
|
||||
const urlString = stringifyUrl(url, queryParams)
|
||||
const options = {
|
||||
method: "DELETE",
|
||||
noInject: true,
|
||||
}
|
||||
return crud(urlString, options)
|
||||
export async function deleteRec(url: string, queryParams: object) {
|
||||
const urlString = stringifyUrl(url, queryParams)
|
||||
const options = {
|
||||
method: "DELETE",
|
||||
noInject: true,
|
||||
}
|
||||
return crud(urlString, options)
|
||||
}
|
||||
|
||||
export async function crud(urlString: string, options: Options) {
|
||||
const environment = process.env.NODE_ENV || "development"
|
||||
const host = urlString.startsWith("http")
|
||||
? ""
|
||||
: 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 }
|
||||
}
|
||||
export async function crud(urlString: string, options: Options) {
|
||||
const environment = process.env.NODE_ENV || "development"
|
||||
const host = urlString.startsWith("http")
|
||||
? ""
|
||||
: 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
|
||||
options.headers[SESSION_TOKEN_NAME] = getCookie(SESSION_TOKEN_NAME)
|
||||
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()
|
||||
const body = await response.text()
|
||||
options.toggleLoader && options.toggleLoader({ [urlString]: false })
|
||||
console.log(err)
|
||||
throw new Error(err.error || err.message, status, extras)
|
||||
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)
|
||||
}
|
||||
|
||||
export function 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 + "?",
|
||||
)
|
||||
: ""
|
||||
export function 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 + "?",
|
||||
)
|
||||
: ""
|
||||
}
|
||||
|
||||
export async function retry(
|
||||
fn: FunctionType,
|
||||
retryOptions: RetryOptions | undefined = {},
|
||||
) {
|
||||
let {
|
||||
retryDescriptor = [1, 3, 5],
|
||||
maxAttempts = 5,
|
||||
onRetry,
|
||||
onSuccess,
|
||||
onFailure,
|
||||
} = retryOptions
|
||||
maxAttempts = Math.min(maxAttempts, MAX_ATTEMPTS_LIMIT)
|
||||
let attempts = 0
|
||||
export async function retry(
|
||||
fn: FunctionType,
|
||||
retryOptions: RetryOptions | undefined = {},
|
||||
) {
|
||||
let {
|
||||
retryDescriptor = [1, 3, 5],
|
||||
maxAttempts = 5,
|
||||
onRetry,
|
||||
onSuccess,
|
||||
onFailure,
|
||||
} = retryOptions
|
||||
maxAttempts = Math.min(maxAttempts, 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
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
return attempt()
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ import { ArContext } from "./contexts"
|
||||
import { isMobile } from "./helper"
|
||||
|
||||
export const ArProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [theme, setTheme] = useState<ArThemes>(ArThemes.DARK1)
|
||||
const [theme, setTheme] = useState<ArThemes>(ArThemes.LIGHT1)
|
||||
const [isLoggedIn, setLoggedIn] = useState<boolean>()
|
||||
const [user, setUser] = useState<User>()
|
||||
const [user, setUser] = useState<User | null>()
|
||||
const [modalState, setModalState] = useState<ModalProps | undefined>(
|
||||
undefined,
|
||||
)
|
||||
@@ -43,6 +43,7 @@ export const ArProvider = ({ children }: { children: ReactNode }) => {
|
||||
setDrawerState,
|
||||
user,
|
||||
setUser,
|
||||
clearUser: () => setUser(null),
|
||||
leftPanelContent,
|
||||
setLeftPanelContent,
|
||||
rightPanelContent,
|
||||
|
||||
71
src/react-app-env.d.ts
vendored
Normal file
71
src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
/// <reference types="node" />
|
||||
/// <reference types="react" />
|
||||
/// <reference types="react-dom" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
readonly NODE_ENV: "development" | "production"
|
||||
readonly PUBLIC_URL: string
|
||||
}
|
||||
}
|
||||
|
||||
declare module "*.avif" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.bmp" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.gif" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.jpg" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.jpeg" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.png" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.webp" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
import * as React from "react"
|
||||
|
||||
export const ReactComponent: React.FunctionComponent<
|
||||
React.SVGProps<SVGSVGElement> & { title?: string }
|
||||
>
|
||||
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.module.css" {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
|
||||
declare module "*.module.scss" {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
|
||||
declare module "*.module.sass" {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
@@ -1,5 +1,16 @@
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { RecursionArgs, ObjectType, RecusionConditionTypes } from "@armco/types"
|
||||
import { FunctionType, ObjectType } from "@armco/types"
|
||||
import { RecusionConditionTypes } from "./enums"
|
||||
|
||||
export interface RecursionArgs {
|
||||
data: any
|
||||
preop?: FunctionType
|
||||
postop?: FunctionType
|
||||
isBrute?: boolean
|
||||
condition?: ObjectType
|
||||
iterateOn?: string
|
||||
}
|
||||
|
||||
|
||||
export function recur(args: RecursionArgs) {
|
||||
const { condition, data, isBrute, preop, postop, iterateOn } = args
|
||||
|
||||
50
src/types.ts
Normal file
50
src/types.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { CSSProperties, ReactNode } from "react"
|
||||
import { ArAlertType } from "./enums"
|
||||
import { BaseProps, FunctionType } from "@armco/types"
|
||||
|
||||
export interface DrawerProps extends BaseProps {
|
||||
children?: ReactNode
|
||||
collapsed?: boolean
|
||||
contentClasses?: string
|
||||
isCollapsible?: boolean
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface ModalProps extends BaseProps {
|
||||
body?: ReactNode
|
||||
closeHandler?: FunctionType
|
||||
content?: ReactNode
|
||||
contentClasses?: string
|
||||
footer?: ReactNode
|
||||
header?: ReactNode
|
||||
hideClose?: boolean
|
||||
isSticky?: boolean
|
||||
modalClasses?: string
|
||||
primaryButtonLabel?: string
|
||||
primaryHandler?: FunctionType
|
||||
secondaryButtonLabel?: string
|
||||
shouldScale?: boolean
|
||||
shouldFadeIn?: boolean
|
||||
show?: boolean
|
||||
}
|
||||
|
||||
// Contexts
|
||||
export interface ArContextType {
|
||||
theme: ArThemes
|
||||
modalState?: ModalProps
|
||||
notification?: AlertProps
|
||||
drawerState?: DrawerProps
|
||||
isLoggedIn?: boolean
|
||||
user?: User | null
|
||||
leftPanelContent?: PanelContent
|
||||
setLeftPanelContent: (leftPanelContent: PanelContent) => void
|
||||
rightPanelContent?: PanelContent
|
||||
setRightPanelContent: (rightPanelContent: PanelContent) => void
|
||||
setTheme: (theme: ArThemes) => void
|
||||
setLoggedIn: (isLoggedIn: boolean) => void
|
||||
setUser: (user: User | null) => void
|
||||
clearUser: () => void
|
||||
setModalState: (modalState: ModalProps | undefined) => void
|
||||
notify: (notification: AlertProps | undefined) => void
|
||||
setDrawerState: (drawerState: DrawerProps) => void
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { ObjectType } from "@armco/types"
|
||||
|
||||
export function validateTask(taskObj: ObjectType) {
|
||||
return (
|
||||
taskObj &&
|
||||
taskObj.name &&
|
||||
taskObj.client &&
|
||||
taskObj.target &&
|
||||
(taskObj.target as ObjectType).endpoint &&
|
||||
taskObj.schedule &&
|
||||
(taskObj.schedule as ObjectType).cron
|
||||
)
|
||||
}
|
||||
@@ -23,5 +23,5 @@
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
, "../taskerclient/src/validators.ts" ],
|
||||
}
|
||||
|
||||
42
vite-dev.config.ts
Normal file
42
vite-dev.config.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
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"
|
||||
import { externalizeDeps } from "vite-plugin-externalize-deps"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), dts({ outDir: "build/types" }), externalizeDeps()],
|
||||
build: {
|
||||
outDir: "build",
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: glob.sync(resolve(__dirname, "src/**/*.{ts,tsx}")),
|
||||
},
|
||||
rollupOptions: {
|
||||
treeshake: true,
|
||||
external: [
|
||||
new RegExp("^react.*"),
|
||||
new RegExp("^@armco/.*"),
|
||||
"@armco/icon",
|
||||
"uuid",
|
||||
"d3",
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: "es",
|
||||
dir: "build/es",
|
||||
entryFileNames: "[name].js",
|
||||
chunkFileNames: "[name].js",
|
||||
},
|
||||
{
|
||||
format: "cjs",
|
||||
dir: "build/cjs",
|
||||
entryFileNames: "[name].js",
|
||||
chunkFileNames: "[name].js",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { resolve } from "node:path"
|
||||
import {glob} from "glob"
|
||||
import { glob } from "glob"
|
||||
import { defineConfig } from "vitest/config"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import dts from "vite-plugin-dts"
|
||||
@@ -15,6 +15,13 @@ export default defineConfig({
|
||||
},
|
||||
rollupOptions: {
|
||||
treeshake: true,
|
||||
external: [
|
||||
new RegExp("^react.*"),
|
||||
new RegExp("^@armco/.*"),
|
||||
"@armco/icon",
|
||||
"uuid",
|
||||
"d3",
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: "es",
|
||||
|
||||
Reference in New Issue
Block a user