@engie-group/fluid-design-system-react
Version:
Fluid Design System React
189 lines (163 loc) • 5.78 kB
text/typescript
import React, { createContext, useContext } from 'react';
const isUndefinedOrNull = (value: unknown): value is null | undefined => {
return typeof value === 'undefined' || value === null;
};
/**
* Generates a string from arguments that can be a string, number, array or objects with {key: value} where key is a class and value is a boolean
*/
const classNames = (...args: unknown[]): string | undefined => {
const classes = [];
for (const arg of args) {
if (!arg) {
continue;
}
const argType = typeof arg;
if (argType === 'string' || argType === 'number') {
classes.push(arg);
} else if (Array.isArray(arg)) {
if (arg.length) {
const inner = Utils.classNames.apply(null, arg);
if (inner) {
classes.push(inner);
}
}
} else if (argType === 'object') {
const typedArg = arg as Record<string, unknown>;
if (typedArg.toString === Object.prototype.toString) {
for (const key in typedArg) {
if (Object.prototype.hasOwnProperty.call(typedArg, key) && typedArg[key]) {
classes.push(key);
}
}
} else {
classes.push(typedArg.toString());
}
}
}
if (classes.length === 0) {
return undefined;
}
return classes.join(' ');
};
const coerceFunction = (func: (...args: unknown[]) => unknown) => {
if (!isUndefinedOrNull(func) && typeof func === 'function') {
return func;
} else {
return null;
}
};
type MergeRefsCallback<T> = (instance: T | null) => void;
type MergeRefsList<T> = Array<React.MutableRefObject<T | null> | React.Ref<T> | null | undefined>;
const mergeRefs = <T = unknown>(refs: MergeRefsList<T>): MergeRefsCallback<T> => {
return (value) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(value);
} else if (ref != null) {
(ref as React.MutableRefObject<T | null>).current = value;
}
});
};
};
const normalizeString = (text: string) => {
return text.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
};
const normalizeAndSearchInText = (text: string, search: string): boolean => {
if (Utils.isUndefinedOrNull(text) || Utils.isUndefinedOrNull(search)) {
return false;
}
const normalizedText = Utils.normalizeString(text);
// removed in 2.13.0: `normalizedSearch` used to filter out (, ) and \ through `.replace(/\(|\)|\\/gi, '');`
const normalizedSearch = Utils.normalizeString(search);
const regExp = new RegExp(Utils.escapeRegExp(normalizedSearch), 'gi');
return normalizedText.search(regExp) !== -1;
};
// Escape special characters in a string (so regex behave properly)
const escapeRegExp = (string: string): string => {
return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
};
const highlightTextAsHtml = (
content: string,
textToHighlight: string,
caseSensitive = false,
escapeAccents = true,
openingTag = '<mark class="nj-highlight">',
closingTag = '</mark>'
): string => {
const regexFlags = caseSensitive ? 'g' : 'gi';
// Escape the textToHighlight to handle special characters
const escapedTextToHighlight = Utils.escapeRegExp(textToHighlight);
let innerHtml: string;
if (Utils.isUndefinedOrNull(textToHighlight)) {
innerHtml = content;
} else {
if (escapeAccents) {
const regExp = new RegExp(Utils.normalizeString(escapedTextToHighlight), regexFlags);
const matches = Utils.normalizeString(content).matchAll(regExp);
let finalText = content;
let buffer = 0;
if (!Utils.isUndefinedOrNull(matches)) {
for (const match of matches) {
const updatedIndex = buffer + match.index;
const textBeforeOccurrence = finalText.slice(0, updatedIndex);
const occurrence = finalText.slice(updatedIndex, updatedIndex + textToHighlight.length);
const textAfterOccurrence = finalText.slice(
updatedIndex + textToHighlight.length,
finalText.length
);
finalText = `${textBeforeOccurrence}${openingTag}${occurrence}${closingTag}${textAfterOccurrence}`;
buffer = buffer + openingTag.length + closingTag.length;
}
}
innerHtml = finalText;
} else {
const regExp = new RegExp(escapedTextToHighlight, regexFlags);
innerHtml = content.replace(regExp, `${openingTag}$&${closingTag}`);
}
}
return innerHtml;
};
const omit = <T extends NonNullable<unknown>, K extends string>(obj: T, ...keys: readonly K[]) => {
return Object.fromEntries(
Object.entries(obj).filter(([key]) => {
return !keys.includes(key as K);
})
) as T;
};
const initializeContext = <T>(
rootComponentNamespace?: string,
message: string = 'Context not provided'
) => {
const InitializedContext = createContext<T | undefined>(undefined);
const useInitializedContext = () => {
const context = useContext(InitializedContext);
if (!context) {
const errorMessage = rootComponentNamespace
? `${rootComponentNamespace} components must be wrapped in <${rootComponentNamespace}Root> or <${rootComponentNamespace}.Root> component`
: message;
throw new Error(errorMessage);
}
return context;
};
return [InitializedContext, useInitializedContext] as const;
};
const initializeSoftContext = <T>() => {
const InitializedContext = createContext<T | undefined>(undefined);
const useInitializedContext = () => {
return useContext(InitializedContext);
};
return [InitializedContext, useInitializedContext] as const;
};
export const Utils = {
isUndefinedOrNull,
classNames,
coerceFunction,
mergeRefs,
normalizeString,
normalizeAndSearchInText,
escapeRegExp,
highlightTextAsHtml,
omit,
initializeContext,
initializeSoftContext
};