UNPKG

15.7 kBJavaScriptView Raw
1import * as ReactDOM from 'react-dom';
2import { SIDE } from './constants';
3/**
4 * @param {string} input - String to capitalize first letter
5 */
6export function capitalize(input) {
7 return input[0].toUpperCase() + input.substring(1);
8}
9/**
10 * @param {string} prefix - String to prefix ID with
11 */
12export function getUniqueId(prefix = 'pf') {
13 const uid = new Date().getTime() +
14 Math.random()
15 .toString(36)
16 .slice(2);
17 return `${prefix}-${uid}`;
18}
19/**
20 * @param { any } this - "This" reference
21 * @param { Function } func - Function to debounce
22 * @param { number } wait - Debounce amount
23 */
24export function debounce(func, wait) {
25 let timeout;
26 return (...args) => {
27 clearTimeout(timeout);
28 timeout = setTimeout(() => func.apply(this, args), wait);
29 };
30}
31/** This function returns whether or not an element is within the viewable area of a container. If partial is true,
32 * then this function will return true even if only part of the element is in view.
33 *
34 * @param {HTMLElement} container The container to check if the element is in view of.
35 * @param {HTMLElement} element The element to check if it is view
36 * @param {boolean} partial true if partial view is allowed
37 * @param {boolean} strict true if strict mode is set, never consider the container width and element width
38 *
39 * @returns { boolean } True if the component is in View.
40 */
41export function isElementInView(container, element, partial, strict = false) {
42 if (!container || !element) {
43 return false;
44 }
45 const containerBounds = container.getBoundingClientRect();
46 const elementBounds = element.getBoundingClientRect();
47 const containerBoundsLeft = Math.ceil(containerBounds.left);
48 const containerBoundsRight = Math.floor(containerBounds.right);
49 const elementBoundsLeft = Math.ceil(elementBounds.left);
50 const elementBoundsRight = Math.floor(elementBounds.right);
51 // Check if in view
52 const isTotallyInView = elementBoundsLeft >= containerBoundsLeft && elementBoundsRight <= containerBoundsRight;
53 const isPartiallyInView = (partial || (!strict && containerBounds.width < elementBounds.width)) &&
54 ((elementBoundsLeft < containerBoundsLeft && elementBoundsRight > containerBoundsLeft) ||
55 (elementBoundsRight > containerBoundsRight && elementBoundsLeft < containerBoundsRight));
56 // Return outcome
57 return isTotallyInView || isPartiallyInView;
58}
59/** This function returns the side the element is out of view on (right, left or both)
60 *
61 * @param {HTMLElement} container The container to check if the element is in view of.
62 * @param {HTMLElement} element The element to check if it is view
63 *
64 * @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.
65 */
66export function sideElementIsOutOfView(container, element) {
67 const containerBounds = container.getBoundingClientRect();
68 const elementBounds = element.getBoundingClientRect();
69 const containerBoundsLeft = Math.floor(containerBounds.left);
70 const containerBoundsRight = Math.floor(containerBounds.right);
71 const elementBoundsLeft = Math.floor(elementBounds.left);
72 const elementBoundsRight = Math.floor(elementBounds.right);
73 // Check if in view
74 const isOffLeft = elementBoundsLeft < containerBoundsLeft;
75 const isOffRight = elementBoundsRight > containerBoundsRight;
76 let side = SIDE.NONE;
77 if (isOffRight && isOffLeft) {
78 side = SIDE.BOTH;
79 }
80 else if (isOffRight) {
81 side = SIDE.RIGHT;
82 }
83 else if (isOffLeft) {
84 side = SIDE.LEFT;
85 }
86 // Return outcome
87 return side;
88}
89/** Interpolates a parameterized templateString using values from a templateVars object.
90 * The templateVars object should have keys and values which match the templateString's parameters.
91 * Example:
92 * const templateString: 'My name is ${firstName} ${lastName}';
93 * const templateVars: {
94 * firstName: 'Jon'
95 * lastName: 'Dough'
96 * };
97 * const result = fillTemplate(templateString, templateVars);
98 * // "My name is Jon Dough"
99 *
100 * @param {string} templateString The string passed by the consumer
101 * @param {object} templateVars The variables passed to the string
102 *
103 * @returns {string} The template string literal result
104 */
105export function fillTemplate(templateString, templateVars) {
106 return templateString.replace(/\${(.*?)}/g, (_, match) => templateVars[match] || '');
107}
108/**
109 * This function allows for keyboard navigation through dropdowns. The custom argument is optional.
110 *
111 * @param {number} index The index of the element you're on
112 * @param {number} innerIndex Inner index number
113 * @param {string} position The orientation of the dropdown
114 * @param {string[]} refsCollection Array of refs to the items in the dropdown
115 * @param {object[]} kids Array of items in the dropdown
116 * @param {boolean} [custom] Allows for handling of flexible content
117 */
118export function keyHandler(index, innerIndex, position, refsCollection, kids, custom = false) {
119 if (!Array.isArray(kids)) {
120 return;
121 }
122 const isMultiDimensional = refsCollection.filter(ref => ref)[0].constructor === Array;
123 let nextIndex = index;
124 let nextInnerIndex = innerIndex;
125 if (position === 'up') {
126 if (index === 0) {
127 // loop back to end
128 nextIndex = kids.length - 1;
129 }
130 else {
131 nextIndex = index - 1;
132 }
133 }
134 else if (position === 'down') {
135 if (index === kids.length - 1) {
136 // loop back to beginning
137 nextIndex = 0;
138 }
139 else {
140 nextIndex = index + 1;
141 }
142 }
143 else if (position === 'left') {
144 if (innerIndex === 0) {
145 nextInnerIndex = refsCollection[index].length - 1;
146 }
147 else {
148 nextInnerIndex = innerIndex - 1;
149 }
150 }
151 else if (position === 'right') {
152 if (innerIndex === refsCollection[index].length - 1) {
153 nextInnerIndex = 0;
154 }
155 else {
156 nextInnerIndex = innerIndex + 1;
157 }
158 }
159 if (refsCollection[nextIndex] === null ||
160 refsCollection[nextIndex] === undefined ||
161 (isMultiDimensional &&
162 (refsCollection[nextIndex][nextInnerIndex] === null || refsCollection[nextIndex][nextInnerIndex] === undefined))) {
163 keyHandler(nextIndex, nextInnerIndex, position, refsCollection, kids, custom);
164 }
165 else if (custom) {
166 if (refsCollection[nextIndex].focus) {
167 refsCollection[nextIndex].focus();
168 }
169 // eslint-disable-next-line react/no-find-dom-node
170 const element = ReactDOM.findDOMNode(refsCollection[nextIndex]);
171 element.focus();
172 }
173 else if (position !== 'tab') {
174 if (isMultiDimensional) {
175 refsCollection[nextIndex][nextInnerIndex].focus();
176 }
177 else {
178 refsCollection[nextIndex].focus();
179 }
180 }
181}
182/** This function returns a list of tabbable items in a container
183 *
184 * @param {any} containerRef to the container
185 * @param {string} tababbleSelectors CSS selector string of tabbable items
186 */
187export function findTabbableElements(containerRef, tababbleSelectors) {
188 const tabbable = containerRef.current.querySelectorAll(tababbleSelectors);
189 const list = Array.prototype.filter.call(tabbable, function (item) {
190 return item.tabIndex >= '0';
191 });
192 return list;
193}
194/** This function is a helper for keyboard navigation through dropdowns.
195 *
196 * @param {number} index The index of the element you're on
197 * @param {string} position The orientation of the dropdown
198 * @param {string[]} collection Array of refs to the items in the dropdown
199 */
200export function getNextIndex(index, position, collection) {
201 let nextIndex;
202 if (position === 'up') {
203 if (index === 0) {
204 // loop back to end
205 nextIndex = collection.length - 1;
206 }
207 else {
208 nextIndex = index - 1;
209 }
210 }
211 else if (index === collection.length - 1) {
212 // loop back to beginning
213 nextIndex = 0;
214 }
215 else {
216 nextIndex = index + 1;
217 }
218 if (collection[nextIndex] === undefined || collection[nextIndex][0] === null) {
219 return getNextIndex(nextIndex, position, collection);
220 }
221 else {
222 return nextIndex;
223 }
224}
225/** This function is a helper for pluralizing strings.
226 *
227 * @param {number} i The quantity of the string you want to pluralize
228 * @param {string} singular The singular version of the string
229 * @param {string} plural The change to the string that should occur if the quantity is not equal to 1.
230 * Defaults to adding an 's'.
231 */
232export function pluralize(i, singular, plural) {
233 if (!plural) {
234 plural = `${singular}s`;
235 }
236 return `${i || 0} ${i === 1 ? singular : plural}`;
237}
238/**
239 * This function is a helper for turning arrays of breakpointMod objects for flex and grid into style object
240 *
241 * @param {object} mods The modifiers object
242 * @param {string} css-variable The appropriate css variable for the component
243 */
244export const setBreakpointCssVars = (mods, cssVar) => Object.entries(mods || {}).reduce((acc, [breakpoint, value]) => breakpoint === 'default' ? Object.assign(Object.assign({}, acc), { [cssVar]: value }) : Object.assign(Object.assign({}, acc), { [`${cssVar}-on-${breakpoint}`]: value }), {});
245/**
246 * This function is a helper for turning arrays of breakpointMod objects for data toolbar and flex into classes
247 *
248 * @param {object} mods The modifiers object
249 * @param {any} styles The appropriate styles object for the component
250 */
251export const formatBreakpointMods = (mods, styles, stylePrefix = '', breakpoint) => {
252 if (!mods) {
253 return '';
254 }
255 if (breakpoint) {
256 if (breakpoint in mods) {
257 return styles.modifiers[toCamel(`${stylePrefix}${mods[breakpoint]}`)];
258 }
259 // the current breakpoint is not specified in mods, so we try to find the next nearest
260 const breakpointsOrder = ['2xl', 'xl', 'lg', 'md', 'sm', 'default'];
261 const breakpointsIndex = breakpointsOrder.indexOf(breakpoint);
262 for (let i = breakpointsIndex; i < breakpointsOrder.length; i++) {
263 if (breakpointsOrder[i] in mods) {
264 return styles.modifiers[toCamel(`${stylePrefix}${mods[breakpointsOrder[i]]}`)];
265 }
266 }
267 return '';
268 }
269 return Object.entries(mods || {})
270 .map(([breakpoint, mod]) => `${stylePrefix}${mod}${breakpoint !== 'default' ? `-on-${breakpoint}` : ''}`)
271 .map(toCamel)
272 .map(mod => mod.replace(/-?(\dxl)/gi, (_res, group) => `_${group}`))
273 .map(modifierKey => styles.modifiers[modifierKey])
274 .filter(Boolean)
275 .join(' ');
276};
277/**
278 * Return the breakpoint for the given width
279 *
280 * @param {number | null} width The width to check
281 * @returns {'default' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'} The breakpoint
282 */
283export const getBreakpoint = (width) => {
284 if (width === null) {
285 return null;
286 }
287 if (width >= 1450) {
288 return '2xl';
289 }
290 if (width >= 1200) {
291 return 'xl';
292 }
293 if (width >= 992) {
294 return 'lg';
295 }
296 if (width >= 768) {
297 return 'md';
298 }
299 if (width >= 576) {
300 return 'sm';
301 }
302 return 'default';
303};
304const camelize = (s) => s
305 .toUpperCase()
306 .replace('-', '')
307 .replace('_', '');
308/**
309 *
310 * @param {string} s string to make camelCased
311 */
312export const toCamel = (s) => s.replace(/([-_][a-z])/gi, camelize);
313/**
314 * Copied from exenv
315 */
316export const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
317/**
318 * Calculate the width of the text
319 * Example:
320 * getTextWidth('my text', node)
321 *
322 * @param {string} text The text to calculate the width for
323 * @param {HTMLElement} node The HTML element
324 */
325export const getTextWidth = (text, node) => {
326 const computedStyle = getComputedStyle(node);
327 // Firefox returns the empty string for .font, so this function creates the .font property manually
328 const getFontFromComputedStyle = () => {
329 let computedFont = '';
330 // Firefox uses percentages for font-stretch, but Canvas does not accept percentages
331 // so convert to keywords, as listed at:
332 // https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch
333 const fontStretchLookupTable = {
334 '50%': 'ultra-condensed',
335 '62.5%': 'extra-condensed',
336 '75%': 'condensed',
337 '87.5%': 'semi-condensed',
338 '100%': 'normal',
339 '112.5%': 'semi-expanded',
340 '125%': 'expanded',
341 '150%': 'extra-expanded',
342 '200%': 'ultra-expanded'
343 };
344 // If the retrieved font-stretch percentage isn't found in the lookup table, use
345 // 'normal' as a last resort.
346 let fontStretch;
347 if (computedStyle.fontStretch in fontStretchLookupTable) {
348 fontStretch = fontStretchLookupTable[computedStyle.fontStretch];
349 }
350 else {
351 fontStretch = 'normal';
352 }
353 computedFont =
354 computedStyle.fontStyle +
355 ' ' +
356 computedStyle.fontVariant +
357 ' ' +
358 computedStyle.fontWeight +
359 ' ' +
360 fontStretch +
361 ' ' +
362 computedStyle.fontSize +
363 '/' +
364 computedStyle.lineHeight +
365 ' ' +
366 computedStyle.fontFamily;
367 return computedFont;
368 };
369 const canvas = document.createElement('canvas');
370 const context = canvas.getContext('2d');
371 context.font = computedStyle.font || getFontFromComputedStyle();
372 return context.measureText(text).width;
373};
374/**
375 * Get the inner dimensions of an element
376 *
377 * @param {HTMLElement} node HTML element to calculate the inner dimensions for
378 */
379export const innerDimensions = (node) => {
380 const computedStyle = getComputedStyle(node);
381 let width = node.clientWidth; // width with padding
382 let height = node.clientHeight; // height with padding
383 height -= parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom);
384 width -= parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
385 return { height, width };
386};
387/**
388 * This function is a helper for truncating text content on the left, leaving the right side of the content in view
389 *
390 * @param {HTMLElement} node HTML element
391 * @param {string} value The original text value
392 */
393export const trimLeft = (node, value) => {
394 const availableWidth = innerDimensions(node).width;
395 let newValue = value;
396 if (getTextWidth(value, node) > availableWidth) {
397 // we have text overflow, trim the text to the left and add ... in the front until it fits
398 while (getTextWidth(`...${newValue}`, node) > availableWidth) {
399 newValue = newValue.substring(1);
400 }
401 // replace text with our truncated text
402 if (node.value) {
403 node.value = `...${newValue}`;
404 }
405 else {
406 node.innerText = `...${newValue}`;
407 }
408 }
409 else {
410 if (node.value) {
411 node.value = value;
412 }
413 else {
414 node.innerText = value;
415 }
416 }
417};
418/**
419 * @param {string[]} events - Operations to prevent when disabled
420 */
421export const preventedEvents = (events) => events.reduce((handlers, eventToPrevent) => (Object.assign(Object.assign({}, handlers), { [eventToPrevent]: (event) => {
422 event.preventDefault();
423 } })), {});
424//# sourceMappingURL=util.js.map
\No newline at end of file