-
Notifications
You must be signed in to change notification settings - Fork 381
Expand file tree
/
Copy pathutil.ts
More file actions
601 lines (558 loc) · 19.2 KB
/
util.ts
File metadata and controls
601 lines (558 loc) · 19.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
import { globalWidthBreakpoints, globalHeightBreakpoints, SIDE } from './constants';
/**
* @param {string} input - String to capitalize first letter
*/
export function capitalize(input: string) {
return input[0].toUpperCase() + input.substring(1);
}
/**
* @param {string} prefix - String to prefix ID with
*/
export function getUniqueId(prefix = 'pf') {
const uid = new Date().getTime() + Math.random().toString(36).slice(2);
return `${prefix}-${uid}`;
}
/**
* @param { any } this - "This" reference
* @param { Function } func - Function to debounce
* @param { number } wait - Debounce amount
*/
export function debounce(this: any, func: (...args: any[]) => any, wait: number) {
let timeout: number;
return (...args: any[]) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait) as any;
};
}
/** This function returns whether or not an element is within the viewable area of a container. If partial is true,
* then this function will return true even if only part of the element is in view.
*
* @param {HTMLElement} container The container to check if the element is in view of.
* @param {HTMLElement} element The element to check if it is view
* @param {boolean} partial true if partial view is allowed
* @param {boolean} strict true if strict mode is set, never consider the container width and element width
*
* @returns { boolean } True if the component is in View.
*/
export function isElementInView(
container: HTMLElement,
element: HTMLElement,
partial: boolean,
strict: boolean = false
): boolean {
if (!container || !element) {
return false;
}
const containerBounds = container.getBoundingClientRect();
const elementBounds = element.getBoundingClientRect();
const containerBoundsLeft = Math.ceil(containerBounds.left);
const containerBoundsRight = Math.floor(containerBounds.right);
const elementBoundsLeft = Math.ceil(elementBounds.left);
const elementBoundsRight = Math.floor(elementBounds.right);
// Check if in view
const isTotallyInView = elementBoundsLeft >= containerBoundsLeft && elementBoundsRight <= containerBoundsRight;
const isPartiallyInView =
(partial || (!strict && containerBounds.width < elementBounds.width)) &&
((elementBoundsLeft < containerBoundsLeft && elementBoundsRight > containerBoundsLeft) ||
(elementBoundsRight > containerBoundsRight && elementBoundsLeft < containerBoundsRight));
// Return outcome
return isTotallyInView || isPartiallyInView;
}
/** This function returns the side the element is out of view on (right, left or both)
*
* @param {HTMLElement} container The container to check if the element is in view of.
* @param {HTMLElement} element The element to check if it is view
*
* @returns {string} right if the element is of the right, left if element is off the left or both if it is off on both sides.
*/
export function sideElementIsOutOfView(container: HTMLElement, element: HTMLElement): string {
const containerBounds = container.getBoundingClientRect();
const elementBounds = element.getBoundingClientRect();
const containerBoundsLeft = Math.floor(containerBounds.left);
const containerBoundsRight = Math.floor(containerBounds.right);
const elementBoundsLeft = Math.floor(elementBounds.left);
const elementBoundsRight = Math.floor(elementBounds.right);
// Check if in view
const isOffLeft = elementBoundsLeft < containerBoundsLeft;
const isOffRight = elementBoundsRight > containerBoundsRight;
let side = SIDE.NONE;
if (isOffRight && isOffLeft) {
side = SIDE.BOTH;
} else if (isOffRight) {
side = SIDE.RIGHT;
} else if (isOffLeft) {
side = SIDE.LEFT;
}
// Return outcome
return side;
}
/** Interpolates a parameterized templateString using values from a templateVars object.
* The templateVars object should have keys and values which match the templateString's parameters.
* Example:
* const templateString: 'My name is ${firstName} ${lastName}';
* const templateVars: {
* firstName: 'Jon'
* lastName: 'Dough'
* };
* const result = fillTemplate(templateString, templateVars);
* // "My name is Jon Dough"
*
* @param {string} templateString The string passed by the consumer
* @param {object} templateVars The variables passed to the string
*
* @returns {string} The template string literal result
*/
export function fillTemplate(templateString: string, templateVars: any) {
return templateString.replace(/\${(.*?)}/g, (_, match) => templateVars[match] || '');
}
/**
* This function allows for keyboard navigation through dropdowns. The custom argument is optional.
*
* @param {number} index The index of the element you're on
* @param {number} innerIndex Inner index number
* @param {string} position The orientation of the dropdown
* @param {string[]} refsCollection Array of refs to the items in the dropdown
* @param {object[]} kids Array of items in the dropdown
* @param {boolean} [custom] Allows for handling of flexible content
*/
export function keyHandler(
index: number,
innerIndex: number,
position: string,
refsCollection: any[],
kids: any[],
custom = false
) {
if (!Array.isArray(kids)) {
return;
}
const isMultiDimensional = refsCollection.filter((ref) => ref)[0].constructor === Array;
let nextIndex = index;
let nextInnerIndex = innerIndex;
if (position === 'up') {
if (index === 0) {
// loop back to end
nextIndex = kids.length - 1;
} else {
nextIndex = index - 1;
}
} else if (position === 'down') {
if (index === kids.length - 1) {
// loop back to beginning
nextIndex = 0;
} else {
nextIndex = index + 1;
}
} else if (position === 'left') {
if (innerIndex === 0) {
nextInnerIndex = refsCollection[index].length - 1;
} else {
nextInnerIndex = innerIndex - 1;
}
} else if (position === 'right') {
if (innerIndex === refsCollection[index].length - 1) {
nextInnerIndex = 0;
} else {
nextInnerIndex = innerIndex + 1;
}
}
if (
refsCollection[nextIndex] === null ||
refsCollection[nextIndex] === undefined ||
(isMultiDimensional &&
(refsCollection[nextIndex][nextInnerIndex] === null || refsCollection[nextIndex][nextInnerIndex] === undefined))
) {
keyHandler(nextIndex, nextInnerIndex, position, refsCollection, kids, custom);
} else if (custom) {
if (refsCollection[nextIndex].focus) {
refsCollection[nextIndex].focus();
}
const element = refsCollection[nextIndex] as HTMLElement;
element.focus();
} else if (position !== 'tab') {
if (isMultiDimensional) {
refsCollection[nextIndex][nextInnerIndex].focus();
} else {
refsCollection[nextIndex].focus();
}
}
}
/** This function returns a list of tabbable items in a container
*
* @param {any} containerRef to the container
* @param {string} tababbleSelectors CSS selector string of tabbable items
*/
export function findTabbableElements(containerRef: any, tababbleSelectors: string): any[] {
const tabbable = containerRef.current.querySelectorAll(tababbleSelectors);
const list = Array.prototype.filter.call(tabbable, function (item) {
return item.tabIndex >= '0';
});
return list;
}
/** This function is a helper for keyboard navigation through dropdowns.
*
* @param {number} index The index of the element you're on
* @param {string} position The orientation of the dropdown
* @param {string[]} collection Array of refs to the items in the dropdown
*/
export function getNextIndex(index: number, position: string, collection: any[]): number {
let nextIndex;
if (position === 'up') {
if (index === 0) {
// loop back to end
nextIndex = collection.length - 1;
} else {
nextIndex = index - 1;
}
} else if (index === collection.length - 1) {
// loop back to beginning
nextIndex = 0;
} else {
nextIndex = index + 1;
}
if (collection[nextIndex] === undefined || collection[nextIndex][0] === null) {
return getNextIndex(nextIndex, position, collection);
} else {
return nextIndex;
}
}
/** This function is a helper for pluralizing strings.
*
* @param {number} i The quantity of the string you want to pluralize
* @param {string} singular The singular version of the string
* @param {string} plural The change to the string that should occur if the quantity is not equal to 1.
* Defaults to adding an 's'.
*/
export function pluralize(i: number, singular: string, plural?: string) {
if (!plural) {
plural = `${singular}s`;
}
return `${i || 0} ${i === 1 ? singular : plural}`;
}
/**
* This function is a helper for turning arrays of breakpointMod objects for flex and grid into style object
*
* @param {object} mods The modifiers object
* @param {string} css-variable The appropriate css variable for the component
*/
export const setBreakpointCssVars = (
mods: {
default?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
'2xl'?: string;
'3xl'?: string;
},
cssVar: string
): React.CSSProperties =>
Object.entries(mods || {}).reduce(
(acc, [breakpoint, value]) =>
breakpoint === 'default' ? { ...acc, [cssVar]: value } : { ...acc, [`${cssVar}-on-${breakpoint}`]: value },
{}
);
export interface Mods {
default?: string;
xs?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
'2xl'?: string;
'3xl'?: string;
'4xl'?: string;
}
/**
* This function is a helper for turning arrays of breakpointMod objects for data toolbar and flex into classes
*
* @param {object} mods The modifiers object
* @param {any} styles The appropriate styles object for the component
*/
export const formatBreakpointMods = (
mods: Mods,
styles: any,
stylePrefix: string = '',
breakpoint?: 'default' | 'sm' | 'md' | 'lg' | 'xl' | '2xl',
vertical?: boolean
) => {
if (!mods) {
return '';
}
if (breakpoint && !vertical) {
if (breakpoint in mods) {
return styles.modifiers[toCamel(`${stylePrefix}${mods[breakpoint as keyof Mods]}`)];
}
// the current breakpoint is not specified in mods, so we try to find the next nearest
const breakpointsOrder = ['2xl', 'xl', 'lg', 'md', 'sm', 'default'];
const breakpointsIndex = breakpointsOrder.indexOf(breakpoint);
for (let i = breakpointsIndex; i < breakpointsOrder.length; i++) {
if (breakpointsOrder[i] in mods) {
return styles.modifiers[toCamel(`${stylePrefix}${mods[breakpointsOrder[i] as keyof Mods]}`)];
}
}
return '';
}
return Object.entries(mods || {})
.map(
([breakpoint, mod]) =>
`${stylePrefix}${mod}${breakpoint !== 'default' ? `-on-${breakpoint}` : ''}${
vertical && breakpoint !== 'default' ? '-height' : ''
}`
)
.map(toCamel)
.map((mod) => mod.replace(/-?(\dxl)/gi, (_res, group) => `_${group}`))
.map((modifierKey) => styles.modifiers[modifierKey])
.filter(Boolean)
.join(' ');
};
/**
* Return the breakpoint for the given height
*
* @param {number | null} height The height to check
* @returns {'default' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'} The breakpoint
*/
export const getVerticalBreakpoint = (height: number): 'default' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' => {
if (height === null) {
return null;
}
if (height >= globalHeightBreakpoints['2xl']) {
return '2xl';
}
if (height >= globalHeightBreakpoints.xl) {
return 'xl';
}
if (height >= globalHeightBreakpoints.lg) {
return 'lg';
}
if (height >= globalHeightBreakpoints.md) {
return 'md';
}
if (height >= globalHeightBreakpoints.sm) {
return 'sm';
}
return 'default';
};
/**
* Return the breakpoint for the given width
*
* @param {number | null} width The width to check
* @returns {'default' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'} The breakpoint
*/
export const getBreakpoint = (width: number): 'default' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' => {
if (width === null) {
return null;
}
if (width >= globalWidthBreakpoints['2xl']) {
return '2xl';
}
if (width >= globalWidthBreakpoints.xl) {
return 'xl';
}
if (width >= globalWidthBreakpoints.lg) {
return 'lg';
}
if (width >= globalWidthBreakpoints.md) {
return 'md';
}
if (width >= globalWidthBreakpoints.sm) {
return 'sm';
}
return 'default';
};
const camelize = (s: string) => s.toUpperCase().replace('-', '').replace('_', '');
/**
*
* @param {string} s string to make camelCased
*/
export const toCamel = (s: string) => s.replace(/([-_][a-z])/gi, camelize);
/**
* Copied from exenv
*/
export const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
/**
* Calculate the width of the text
* Example:
* getTextWidth('my text', node)
*
* @param {string} text The text to calculate the width for
* @param {HTMLElement} node The HTML element
*/
export const getTextWidth = (text: string, node: HTMLElement) => {
const computedStyle = getComputedStyle(node);
// Firefox returns the empty string for .font, so this function creates the .font property manually
const getFontFromComputedStyle = () => {
let computedFont = '';
// Firefox uses percentages for font-stretch, but Canvas does not accept percentages
// so convert to keywords, as listed at:
// https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch
const fontStretchLookupTable = {
'50%': 'ultra-condensed',
'62.5%': 'extra-condensed',
'75%': 'condensed',
'87.5%': 'semi-condensed',
'100%': 'normal',
'112.5%': 'semi-expanded',
'125%': 'expanded',
'150%': 'extra-expanded',
'200%': 'ultra-expanded'
};
// If the retrieved font-stretch percentage isn't found in the lookup table, use
// 'normal' as a last resort.
let fontStretch;
if (computedStyle.fontStretch in fontStretchLookupTable) {
fontStretch = (fontStretchLookupTable as any)[computedStyle.fontStretch];
} else {
fontStretch = 'normal';
}
computedFont =
computedStyle.fontStyle +
' ' +
computedStyle.fontVariant +
' ' +
computedStyle.fontWeight +
' ' +
fontStretch +
' ' +
computedStyle.fontSize +
'/' +
computedStyle.lineHeight +
' ' +
computedStyle.fontFamily;
return computedFont;
};
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = computedStyle.font || getFontFromComputedStyle();
return context.measureText(text).width;
};
/**
* Get the inner dimensions of an element
*
* @param {HTMLElement} node HTML element to calculate the inner dimensions for
*/
export const innerDimensions = (node: HTMLElement) => {
const computedStyle = getComputedStyle(node);
let width = node.clientWidth; // width with padding
let height = node.clientHeight; // height with padding
height -= parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom);
width -= parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
return { height, width };
};
/**
* This function is a helper for truncating text content on the left, leaving the right side of the content in view
*
* @param {HTMLElement} node HTML element
* @param {string} value The original text value
*/
export const trimLeft = (node: HTMLElement, value: string) => {
const availableWidth = innerDimensions(node).width;
let newValue = value;
if (getTextWidth(value, node) > availableWidth) {
// we have text overflow, trim the text to the left and add ... in the front until it fits
while (getTextWidth(`...${newValue}`, node) > availableWidth) {
newValue = newValue.substring(1);
}
// replace text with our truncated text
if ((node as HTMLInputElement).value) {
(node as HTMLInputElement).value = `...${newValue}`;
} else {
node.innerText = `...${newValue}`;
}
} else {
if ((node as HTMLInputElement).value) {
(node as HTMLInputElement).value = value;
} else {
node.innerText = value;
}
}
};
/**
* @param {string[]} events - Operations to prevent when disabled
*/
export const preventedEvents = (events: string[]) =>
events.reduce(
(handlers, eventToPrevent) => ({
...handlers,
[eventToPrevent]: (event: React.SyntheticEvent<HTMLElement>) => {
event.preventDefault();
}
}),
{}
);
/**
* @param {React.RefObject<any>[]} timeoutRefs - Timeout refs to clear
*/
export const clearTimeouts = (timeoutRefs: React.RefObject<any>[]) => {
timeoutRefs.forEach((ref) => {
if (ref.current) {
clearTimeout(ref.current);
}
});
};
/**
* @param {React.RefObject<number>} animationFrameRef - Animation frame ref to clear
*/
export const clearAnimationFrame = (animationFrameRef: React.RefObject<number>) => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
/**
* Helper function to get the language direction of a given element, useful for figuring out if left-to-right
* or right-to-left specific logic should be applied.
*
* @param {HTMLElement} targetElement - Element the helper will get the language direction of
* @param {'ltr' | 'rtl'} defaultDirection - Language direction to assume if one can't be determined, defaults to 'ltr'
* @returns {'ltr' | 'rtl'} - The language direction of the target element
*/
export const getLanguageDirection = (targetElement: HTMLElement, defaultDirection: 'ltr' | 'rtl' = 'ltr') => {
if (!targetElement) {
return defaultDirection;
}
const computedDirection = getComputedStyle(targetElement).getPropertyValue('direction');
if (['ltr', 'rtl'].includes(computedDirection)) {
return computedDirection as 'ltr' | 'rtl';
}
return defaultDirection;
};
/**
* Gets a reference element based on a ref property, which can typically be 1 of several types.
*
* @param {HTMLElement | (() => HTMLElement) | React.RefObject<any>} refProp The ref property to get a reference element from.
* @returns The reference element if one is found.
*/
export const getReferenceElement = (refProp: HTMLElement | (() => HTMLElement) | React.RefObject<any>) => {
if (refProp instanceof HTMLElement) {
return refProp;
}
if (typeof refProp === 'function') {
return refProp();
}
return refProp?.current;
};
/**
* Gets the [client|offset|scroll]Left property of an element, and determines whether it needs to be
* adjusted for an RTL direction.
*
* @param {HTMLElement} targetElement - Element to get the inline-start property of.
* @param {HTMLElement} ancestorElement - Ancestor element to base the inline-start calculation off of when the direction is RTL.
* @param {'client' | 'offset' | 'scroll'} inlineType - The inline-start property type to base calculations on.
* @returns {number} - The value of the inline-start property.
*/
export const getInlineStartProperty = (
targetElement: HTMLElement,
ancestorElement: HTMLElement,
inlineType: 'client' | 'offset' | 'scroll' = 'offset'
) => {
if (!targetElement) {
return;
}
const inlineProperty: 'offsetLeft' | 'clientLeft' | 'scrollLeft' = `${inlineType}Left`;
const isRTL = getLanguageDirection(targetElement) === 'rtl';
if (!isRTL) {
return targetElement[inlineProperty];
}
const widthProperty: 'offsetWidth' | 'clientWidth' | 'scrollWidth' = `${inlineType}Width`;
return ancestorElement[widthProperty] - (targetElement[inlineProperty] + targetElement[widthProperty]);
};