2023-05-17 17:32:19 +02:00
|
|
|
import tlds from 'tlds/index.json' assert { type: 'json' }
|
2022-05-31 10:12:54 +02:00
|
|
|
import get from 'lodash.get'
|
2022-08-29 17:44:50 +02:00
|
|
|
import set from 'lodash.set'
|
2022-09-29 14:45:09 +02:00
|
|
|
import orderBy from 'lodash.orderby'
|
2023-05-13 14:17:47 +02:00
|
|
|
import unset from 'lodash.unset'
|
2023-08-08 10:17:01 +02:00
|
|
|
import { cloudflareConfig } from './config/cloudflare.mjs'
|
2022-05-31 10:12:54 +02:00
|
|
|
|
2023-05-19 09:22:11 +02:00
|
|
|
// Method that returns a unique ID when all you need is an ID
|
|
|
|
// but you can't be certain you have one
|
|
|
|
export const getId = (id) => (id ? id : Date.now())
|
|
|
|
|
2022-01-25 18:14:31 +01:00
|
|
|
// Generic rounding method
|
2022-12-29 12:39:58 +01:00
|
|
|
export const round = (val, decimals = 1) =>
|
|
|
|
Math.round(val * Math.pow(10, decimals)) / Math.pow(10, decimals)
|
2022-01-25 18:14:31 +01:00
|
|
|
|
|
|
|
// Rounds a value in mm
|
|
|
|
export const roundMm = (val, units) => {
|
2022-12-29 12:39:58 +01:00
|
|
|
if (units === 'imperial') return Math.round(val * 1000000) / 1000000
|
|
|
|
else return Math.round(val * 10) / 10
|
2022-01-25 18:14:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Formatting for imperial values
|
|
|
|
export const formatImperial = (neg, inch, numo = false, deno = false, format = 'html') => {
|
|
|
|
if (format === 'html') {
|
2023-05-29 13:35:04 -05:00
|
|
|
if (numo) return `${neg}${inch} <sup>${numo}</sup>/<sub>${deno}</sub>"`
|
2023-04-23 18:00:52 +02:00
|
|
|
else return `${neg}${inch}"`
|
2022-06-24 20:33:18 +02:00
|
|
|
} else if (format === 'notags') {
|
2023-05-29 13:35:04 -05:00
|
|
|
if (numo) return `${neg}${inch} ${numo}/${deno}"`
|
2022-06-24 20:33:18 +02:00
|
|
|
else return `${neg}${inch}"`
|
2022-01-25 18:14:31 +01:00
|
|
|
} else {
|
|
|
|
if (numo) return `${neg}${inch} ${numo}/${deno}`
|
|
|
|
else return `${neg}${inch}`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-18 16:18:35 +02:00
|
|
|
/**
|
|
|
|
* format a value to the nearest fraction with a denominator that is a power of 2
|
|
|
|
* or a decimal if the value is between fractions
|
|
|
|
* NOTE: this method does not convert mm to inches. It will turn any given value directly into its equivalent fractional representation
|
|
|
|
*
|
|
|
|
* fraction: the value to process
|
|
|
|
* format: the type of formatting to apply. html, notags, or anything else which will only return numbers
|
|
|
|
*/
|
|
|
|
export const formatFraction128 = (fraction, format = 'html') => {
|
|
|
|
let negative = ''
|
|
|
|
let inches = ''
|
|
|
|
let rest = ''
|
|
|
|
if (fraction < 0) {
|
|
|
|
fraction = fraction * -1
|
|
|
|
negative = '-'
|
|
|
|
}
|
|
|
|
if (Math.abs(fraction) < 1) rest = fraction
|
|
|
|
else {
|
|
|
|
inches = Math.floor(fraction)
|
|
|
|
rest = fraction - inches
|
|
|
|
}
|
|
|
|
let fraction128 = Math.round(rest * 128)
|
2023-06-22 10:32:01 -05:00
|
|
|
if (fraction128 == 0) return formatImperial(negative, inches || fraction128, false, false, format)
|
2023-02-18 16:18:35 +02:00
|
|
|
|
|
|
|
for (let i = 1; i < 7; i++) {
|
|
|
|
const numoFactor = Math.pow(2, 7 - i)
|
|
|
|
if (fraction128 % numoFactor === 0)
|
|
|
|
return formatImperial(negative, inches, fraction128 / numoFactor, Math.pow(2, i), format)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
negative +
|
|
|
|
Math.round(fraction * 100) / 100 +
|
|
|
|
(format === 'html' || format === 'notags' ? '"' : '')
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-01-25 18:14:31 +01:00
|
|
|
// Format a value in mm based on the user's units
|
2022-06-24 20:33:18 +02:00
|
|
|
// Format can be html, notags, or anything else which will only return numbers
|
2022-01-25 18:14:31 +01:00
|
|
|
export const formatMm = (val, units, format = 'html') => {
|
|
|
|
val = roundMm(val)
|
|
|
|
if (units === 'imperial') {
|
|
|
|
if (val == 0) return formatImperial('', 0, false, false, format)
|
|
|
|
|
2023-02-18 16:18:35 +02:00
|
|
|
let fraction = val / 25.4
|
|
|
|
return formatFraction128(fraction, format)
|
2022-01-25 18:14:31 +01:00
|
|
|
} else {
|
2022-06-24 20:33:18 +02:00
|
|
|
if (format === 'html' || format === 'notags') return roundMm(val / 10) + 'cm'
|
2022-01-25 18:14:31 +01:00
|
|
|
else return roundMm(val / 10)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Format a percentage (as in, between 0 and 1)
|
2022-12-29 12:39:58 +01:00
|
|
|
export const formatPercentage = (val) => Math.round(1000 * val) / 10 + '%'
|
2022-01-25 18:14:31 +01:00
|
|
|
|
2022-12-29 12:39:58 +01:00
|
|
|
export const optionType = (option) => {
|
2022-01-25 18:14:31 +01:00
|
|
|
if (typeof option?.pct !== 'undefined') return 'pct'
|
|
|
|
if (typeof option?.bool !== 'undefined') return 'bool'
|
|
|
|
if (typeof option?.count !== 'undefined') return 'count'
|
|
|
|
if (typeof option?.deg !== 'undefined') return 'deg'
|
|
|
|
if (typeof option?.list !== 'undefined') return 'list'
|
|
|
|
if (typeof option?.mm !== 'undefined') return 'mm'
|
|
|
|
|
|
|
|
return 'constant'
|
|
|
|
}
|
|
|
|
|
2023-05-08 19:28:03 +02:00
|
|
|
export const capitalize = (string) =>
|
|
|
|
typeof string === 'string' ? string.charAt(0).toUpperCase() + string.slice(1) : ''
|
2022-02-07 19:51:08 +01:00
|
|
|
|
2023-01-09 21:02:08 +01:00
|
|
|
export const getCrumbs = (app, slug = false) => {
|
2022-05-31 10:12:54 +02:00
|
|
|
if (!slug) return null
|
|
|
|
const crumbs = []
|
|
|
|
const chunks = slug.split('/')
|
|
|
|
for (const i in chunks) {
|
2022-12-29 12:39:58 +01:00
|
|
|
const j = parseInt(i) + parseInt(1)
|
|
|
|
const page = get(app.navigation, chunks.slice(0, j))
|
|
|
|
if (page) crumbs.push([page.__linktitle, '/' + chunks.slice(0, j).join('/'), j < chunks.length])
|
2022-05-31 10:12:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return crumbs
|
|
|
|
}
|
|
|
|
|
2023-05-31 17:42:16 -05:00
|
|
|
/** convert a millimeter value to a Number value in the given units */
|
2023-05-29 13:35:04 -05:00
|
|
|
export const measurementAsUnits = (mmValue, units = 'metric') =>
|
2023-07-26 16:38:51 -06:00
|
|
|
round(mmValue / (units === 'imperial' ? 25.4 : 10), 3)
|
|
|
|
|
|
|
|
/** convert a value that may contain a fraction to a decimal */
|
|
|
|
export const fractionToDecimal = (value) => {
|
|
|
|
// if it's just a number, return it
|
|
|
|
if (!isNaN(value)) return value
|
|
|
|
|
|
|
|
// keep a running total
|
|
|
|
let total = 0
|
|
|
|
|
|
|
|
// split by spaces
|
|
|
|
let chunks = String(value).split(' ')
|
|
|
|
if (chunks.length > 2) return Number.NaN // too many spaces to parse
|
|
|
|
|
|
|
|
// a whole number with a fraction
|
|
|
|
if (chunks.length === 2) {
|
|
|
|
// shift the whole number from the array
|
|
|
|
const whole = Number(chunks.shift())
|
|
|
|
// if it's not a number, return NaN
|
|
|
|
if (isNaN(whole)) return Number.NaN
|
|
|
|
// otherwise add it to the total
|
|
|
|
total += whole
|
|
|
|
}
|
|
|
|
|
|
|
|
// now we have only one chunk to parse
|
|
|
|
let fraction = chunks[0]
|
|
|
|
|
|
|
|
// split it to get numerator and denominator
|
|
|
|
let fChunks = fraction.trim().split('/')
|
|
|
|
// not really a fraction. return NaN
|
|
|
|
if (fChunks.length !== 2 || fChunks[1] === '') return Number.NaN
|
|
|
|
|
|
|
|
// do the division
|
|
|
|
let num = Number(fChunks[0])
|
|
|
|
let denom = Number(fChunks[1])
|
|
|
|
if (isNaN(num) || isNaN(denom)) return NaN
|
|
|
|
return total + num / denom
|
|
|
|
}
|
2023-05-29 13:35:04 -05:00
|
|
|
|
2022-12-29 12:39:58 +01:00
|
|
|
export const measurementAsMm = (value, units = 'metric') => {
|
|
|
|
if (typeof value === 'number') return value * (units === 'imperial' ? 25.4 : 10)
|
2022-07-03 12:55:46 +02:00
|
|
|
|
2023-07-26 16:38:51 -06:00
|
|
|
if (String(value).endsWith('.')) return false
|
2022-07-03 12:55:46 +02:00
|
|
|
|
2022-12-29 12:39:58 +01:00
|
|
|
if (units === 'metric') {
|
|
|
|
value = Number(value)
|
|
|
|
if (isNaN(value)) return false
|
|
|
|
return value * 10
|
2022-07-03 12:55:46 +02:00
|
|
|
} else {
|
2023-07-26 16:38:51 -06:00
|
|
|
const decimal = fractionToDecimal(value)
|
|
|
|
if (isNaN(decimal)) return false
|
|
|
|
return decimal * 24.5
|
2022-07-03 12:55:46 +02:00
|
|
|
}
|
|
|
|
}
|
2022-08-29 17:44:50 +02:00
|
|
|
|
2022-12-29 12:39:58 +01:00
|
|
|
export const optionsMenuStructure = (options) => {
|
2022-08-29 17:44:50 +02:00
|
|
|
if (!options) return options
|
2022-09-29 14:45:09 +02:00
|
|
|
const sorted = {}
|
2022-08-29 17:44:50 +02:00
|
|
|
for (const [name, option] of Object.entries(options)) {
|
2023-05-03 14:26:17 -07:00
|
|
|
if (typeof option === 'object') sorted[name] = { ...option, name }
|
2022-09-29 14:45:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const menu = {}
|
|
|
|
// Fixme: One day we should sort this based on the translation
|
|
|
|
for (const option of orderBy(sorted, ['menu', 'name'], ['asc'])) {
|
2022-12-29 12:39:58 +01:00
|
|
|
if (typeof option === 'object') {
|
2023-06-22 10:32:01 -05:00
|
|
|
const oType = optionType(option)
|
|
|
|
option.dflt = option.dflt || option[oType]
|
|
|
|
if (oType === 'pct') option.dflt /= 100
|
2023-05-29 22:34:33 -05:00
|
|
|
if (option.menu) {
|
2023-05-31 17:42:16 -05:00
|
|
|
set(menu, `${option.menu}.isGroup`, true)
|
2023-05-29 22:34:33 -05:00
|
|
|
set(menu, `${option.menu}.${option.name}`, option)
|
|
|
|
} else if (typeof option.menu === 'undefined') {
|
2022-12-29 12:39:58 +01:00
|
|
|
console.log(
|
|
|
|
`Warning: Option ${option.name} does not have a menu config. ` +
|
2023-02-05 17:59:22 +01:00
|
|
|
'Either configure it, or set it to false to hide this option.'
|
2022-12-29 12:39:58 +01:00
|
|
|
)
|
2023-02-12 11:26:25 -06:00
|
|
|
}
|
2022-08-29 17:44:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-29 14:45:09 +02:00
|
|
|
// Always put advanced at the end
|
|
|
|
if (menu.advanced) {
|
|
|
|
const adv = menu.advanced
|
|
|
|
delete menu.advanced
|
|
|
|
menu.advanced = adv
|
|
|
|
}
|
|
|
|
|
2022-08-29 17:44:50 +02:00
|
|
|
return menu
|
|
|
|
}
|
2023-05-13 14:17:47 +02:00
|
|
|
|
|
|
|
// Helper method to handle object updates
|
|
|
|
export const objUpdate = (obj = {}, path, val = 'unset') => {
|
|
|
|
if (val === 'unset') {
|
|
|
|
if (Array.isArray(path) && Array.isArray(path[0])) {
|
|
|
|
for (const [ipath, ival = 'unset'] of path) {
|
|
|
|
if (ival === 'unset') unset(obj, ipath)
|
|
|
|
else set(obj, ipath, ival)
|
|
|
|
}
|
|
|
|
} else unset(obj, path)
|
|
|
|
} else set(obj, path, val)
|
|
|
|
|
|
|
|
return obj
|
|
|
|
}
|
2023-05-17 17:32:19 +02:00
|
|
|
|
|
|
|
/** Validates an email address for correct syntax */
|
|
|
|
export const validateEmail = (email) => {
|
|
|
|
/* eslint-disable */
|
|
|
|
const re =
|
|
|
|
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
|
|
|
/* eslint-enable */
|
|
|
|
return re.test(email)
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Validates the top level domain (TLT) for an email address */
|
|
|
|
export const validateTld = (email) => {
|
|
|
|
const tld = email.split('@').pop().split('.').pop().toLowerCase()
|
|
|
|
if (tlds.indexOf(tld) === -1) return tld
|
|
|
|
else return true
|
|
|
|
}
|
2023-05-22 19:51:47 +02:00
|
|
|
|
|
|
|
export const nsMerge = (...args) => {
|
|
|
|
const ns = new Set()
|
|
|
|
for (const arg of args) {
|
|
|
|
if (typeof arg === 'string') ns.add(arg)
|
2023-06-10 20:33:34 +02:00
|
|
|
else if (Array.isArray(arg)) {
|
|
|
|
for (const el of nsMerge(...arg)) ns.add(el)
|
|
|
|
} else console.log('Unexpected namespect type:', { arg })
|
2023-05-22 19:51:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return [...ns]
|
|
|
|
}
|
2023-05-26 15:54:43 +02:00
|
|
|
|
|
|
|
export const shortDate = (locale = 'en', timestamp = false) => {
|
|
|
|
const options = {
|
|
|
|
year: 'numeric',
|
|
|
|
month: 'short',
|
|
|
|
day: 'numeric',
|
|
|
|
hour: '2-digit',
|
|
|
|
minute: '2-digit',
|
|
|
|
hour12: false,
|
|
|
|
}
|
|
|
|
const ts = timestamp ? new Date(timestamp) : new Date()
|
|
|
|
|
|
|
|
return ts.toLocaleDateString(locale, options)
|
|
|
|
}
|
2023-06-17 17:29:28 +02:00
|
|
|
|
2023-07-19 19:08:41 +02:00
|
|
|
export const yyyymmdd = (timestamp = false) => {
|
|
|
|
const ts = timestamp ? new Date(timestamp) : new Date()
|
|
|
|
|
|
|
|
let m = String(ts.getMonth() + 1)
|
|
|
|
if (m.length === 1) m = '0' + m
|
|
|
|
let d = '' + ts.getDate()
|
|
|
|
if (d.length === 1) d = '0' + d
|
|
|
|
|
|
|
|
return `${ts.getFullYear()}${m}${d}`
|
|
|
|
}
|
|
|
|
|
2023-06-17 17:29:28 +02:00
|
|
|
export const scrollTo = (id) => {
|
2023-06-17 17:42:59 +02:00
|
|
|
// eslint-disable-next-line no-undef
|
2023-07-15 10:41:58 +02:00
|
|
|
const el = document ? document.getElementById(id) : null
|
|
|
|
if (el) el.scrollIntoView()
|
2023-06-17 17:29:28 +02:00
|
|
|
}
|
2023-06-20 17:30:18 +02:00
|
|
|
|
2023-06-26 18:01:53 +02:00
|
|
|
const structureMeasurementsAsDesign = (measurements) => ({ patternConfig: { measurements } })
|
|
|
|
|
|
|
|
export const designMeasurements = (Design, measies = {}, DesignIsMeasurementsPojo = false) => {
|
|
|
|
if (DesignIsMeasurementsPojo) Design = structureMeasurementsAsDesign(Design)
|
2023-06-20 20:19:31 +02:00
|
|
|
const measurements = {}
|
|
|
|
for (const m of Design.patternConfig?.measurements || []) measurements[m] = measies[m]
|
|
|
|
for (const m of Design.patternConfig?.optionalMeasurements || []) measurements[m] = measies[m]
|
|
|
|
|
|
|
|
return measurements
|
|
|
|
}
|
|
|
|
|
2023-06-26 18:01:53 +02:00
|
|
|
export const hasRequiredMeasurements = (Design, measies = {}, DesignIsMeasurementsPojo = false) => {
|
|
|
|
if (DesignIsMeasurementsPojo) Design = structureMeasurementsAsDesign(Design)
|
2023-06-20 17:30:18 +02:00
|
|
|
const missing = []
|
|
|
|
for (const m of Design.patternConfig?.measurements || []) {
|
|
|
|
if (typeof measies[m] === 'undefined') missing.push(m)
|
|
|
|
}
|
|
|
|
|
|
|
|
return [missing.length === 0, missing]
|
|
|
|
}
|
2023-07-14 09:23:37 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
* This expects a object from the nav tree and will filter out the know 1-char keys
|
|
|
|
* and then check if there are any left. If there are, those are child-pages.
|
|
|
|
*/
|
|
|
|
export const pageHasChildren = (page) =>
|
|
|
|
Object.keys(page).filter((key) => !['t', 's', 'o', 'b', 'h'].includes(key)).length > 0
|
2023-07-15 10:41:58 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Returns the slug of the page above this one
|
|
|
|
* Or the current slug if there is no higher slug
|
|
|
|
*/
|
|
|
|
export const oneUpSlug = (slug) => {
|
|
|
|
const chunks = slug.split('/')
|
|
|
|
|
|
|
|
return chunks.length > 1 ? chunks.slice(0, -1).join('/') : slug
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Returns the slug at the max depth of the navigation root
|
|
|
|
* We don't descend too far into the navigation because it becomes harder to find your way back
|
|
|
|
*/
|
|
|
|
export const maxPovDepthSlug = (slug, site) => {
|
|
|
|
// Default depth
|
|
|
|
let depth = 2
|
|
|
|
|
|
|
|
// Split the slug
|
|
|
|
const chunks = slug.split('/')
|
|
|
|
|
|
|
|
// Some specific exceptions
|
|
|
|
if (site === 'org') {
|
|
|
|
if (chunks[0] === 'docs' && chunks[1] === 'designs') depth = 3
|
|
|
|
}
|
|
|
|
|
|
|
|
return chunks.length > depth ? chunks.slice(0, depth).join('/') : slug
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Checks whether one slug is part of another.
|
|
|
|
* Typically used to see if a page is 'active' on the path to another page.
|
|
|
|
* Eg: the user is on page reference/api/part so reference/api is on the way to that page
|
|
|
|
* In that case, this will return true
|
|
|
|
*/
|
2023-07-26 18:49:54 +02:00
|
|
|
export const isSlugPart = (part, slug) => slug && part && slug.slice(0, part.length) === part
|
2023-07-18 21:27:36 -06:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Makes a properly formated path for the given locale
|
|
|
|
* (i.e. skips adding 'en' to localized paths)
|
|
|
|
* Expects a slug with no leading slash
|
|
|
|
* */
|
|
|
|
export const localePath = (locale, slug) => (locale === 'en' ? '/' : `/${locale}/`) + slug
|
2023-07-30 14:21:39 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Formats a number for display to human beings. Keeps long/high numbers short
|
|
|
|
*/
|
|
|
|
export const formatNumber = (num, suffix = '') => {
|
|
|
|
if (num === null || typeof num === 'undefined') return num
|
|
|
|
// Small values don't get formatted
|
|
|
|
if (num < 1) return num
|
|
|
|
if (num) {
|
|
|
|
const sizes = ['', 'K', 'M', 'B']
|
|
|
|
const i = Math.min(
|
|
|
|
parseInt(Math.floor(Math.log(num) / Math.log(1000)).toString(), 10),
|
|
|
|
sizes.length - 1
|
|
|
|
)
|
|
|
|
return `${(num / 1000 ** i).toFixed(i ? 1 : 0)}${sizes[i]}${suffix}`
|
|
|
|
}
|
|
|
|
|
|
|
|
return '0'
|
|
|
|
}
|
2023-08-08 10:17:01 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Returns the URL of a cloudflare image
|
|
|
|
* based on the ID and Variant
|
|
|
|
*/
|
|
|
|
export const cloudflareImageUrl = ({ id = 'default-avatar', variant = 'public' }) => {
|
|
|
|
/*
|
|
|
|
* If the variant is invalid, set it to the smallest thumbnail so
|
|
|
|
* people don't load enourmous images by accident
|
|
|
|
*/
|
|
|
|
if (!cloudflareConfig.variants.includes(variant)) variant = 'sq100'
|
|
|
|
|
|
|
|
return `${cloudflareConfig.url}${id}/${variant}`
|
|
|
|
}
|