import React, { CSSProperties, ElementType, ReactElement, ReactNode } from 'react'; import { RuleSet, css } from 'styled-components'; import { CssMap, CssProps, CssResponsiveProps } from './css'; import { ThemeProps } from './types'; export const ObjectKeys = Object.keys as (o: T) => (Extract)[]; export function eachMedia(props: ThemeProps, callback: (key: string) => string | void) { const theme = props.theme; if (theme.mediaQuery && typeof callback === 'function') { return ObjectKeys(theme.mediaQuery).map(key => { const value = theme.mediaQuery[key]; const rule = callback(key); return rule ? `@media(min-width: ${value}) { ${rule} }` : ''; }).join('\n'); } else { return ''; } } export function mediaMin(props: ThemeProps, key: keyof typeof props.theme.mediaQuery): CSSProperties['width'] | undefined { return props.theme.mediaQuery[key]; } export function mediaMax(props: ThemeProps, key: keyof typeof props.theme.mediaQuery): CSSProperties['width'] | undefined { const keys = Object.keys(props.theme.mediaQuery); const index = keys.indexOf(key); const nextKey = index !== -1 && index < keys.length - 1 ? keys[index + 1] : undefined; return nextKey !== undefined ? `${(parseInt(String(props.theme.mediaQuery[nextKey])) - .02)}px` : undefined; } export function mediaUp(props: ThemeProps, key: keyof typeof props.theme.mediaQuery, rule: RuleSet): RuleSet { const min = mediaMin(props, key); return css`${min ? css`@media(min-width: ${min}) { ${rule} }` : ''}`; } export function mediaDown(props: ThemeProps, key: keyof typeof props.theme.mediaQuery, rule: RuleSet): RuleSet { const max = mediaMax(props, key); return css`${max ? `@media(max-width: ${max}) { ${rule} }` : rule}`; } export function mediaBetween(props: ThemeProps, minKey: keyof typeof props.theme.mediaQuery, maxKey: keyof typeof props.theme.mediaQuery, rule: RuleSet): RuleSet { const min = mediaMin(props, minKey); const max = mediaMax(props, maxKey); if (min && max) { return css`@media (min-width: ${min}) and (max-width: ${max}) { ${rule} }`; } else if (!max) { return css`@media(min-width: ${min}) { ${rule} }`; } else { return css`@media(max-width: ${max}) { ${rule} }`; } } export function mediaOnly(props: ThemeProps, key: keyof typeof props.theme.mediaQuery, rule: RuleSet): RuleSet { return mediaBetween(props, key, key, rule); } export function getCssResponsive(props: CssResponsiveProps & ThemeProps, defaultValue: CssResponsiveProps = {}) { props = { ...defaultValue, ...props }; let rules = ''; for (const [key, value] of Object.entries(props)) { // console.log(`${key}: ${value}`); const rule = key.replace(/(.+?)(Sm|Md|Lg|Xl)?$/, function (m, g1, g2) { const prop: string = g1; if (CssMap.has(prop)) { // console.log('prop', prop); const rule = `${CssMap.get(prop)}: ${value};`; if (g2) { const size: keyof typeof props.theme.mediaQuery = g2.toLowerCase(); // console.log('size', size, 'rule', rule); return `@media(min-width: ${props.theme.mediaQuery[size]}) { ${rule} }`; } else { // console.log('rule', rule); return rule; } } else { return ''; } }); rules += rule + '\n'; } return rules; } export function isCssProp(prop: string): boolean { const replacedProp = prop.replace(/(Sm|Md|Lg|Xl)$/, ''); // console.log('isCssProp', replacedProp, CssMap.has(replacedProp)); return CssMap.has(replacedProp); } export function shouldForwardProp(prop: string): boolean { // console.log(prop, 'isCssProp', isCssProp(prop)); return !isCssProp(prop); } export function getAspectResponsive(props: CssResponsiveProps & ThemeProps, defaultValue: CssResponsiveProps = {}) { props = { ...defaultValue, ...props }; let rules = ''; for (const [key, value] of Object.entries(props)) { const rule = key.replace(/(.+?)(Sm|Md|Lg|Xl)?$/, function (m, g1, g2) { const prop = g1; if (prop === 'aspectRatio') { const rule = ` position: relative; overflow: hidden; `; if (g2) { const size: keyof typeof props.theme.mediaQuery = g2.toLowerCase(); // console.log('size', size, 'rule', rule); return `@media(min-width: ${props.theme.mediaQuery[size]}) { ${rule} }`; } else { // console.log('rule', rule); return rule; } } else { return ''; } }); rules += rule + '\n'; } return rules; } export function pickResponsiveProps(props: CssResponsiveProps, ...keys: (keyof CssProps)[]) { const displayProps: CssResponsiveProps = {}; const otherProps: CssResponsiveProps = {}; const responsiveKeys: (keyof CssResponsiveProps)[] = []; keys.forEach(k => { responsiveKeys.push(k); responsiveKeys.push(`${k}Sm`); responsiveKeys.push(`${k}Md`); responsiveKeys.push(`${k}Lg`); responsiveKeys.push(`${k}Xl`); }); Object.entries(props).forEach(([k, v]) => { const key = k as keyof CssResponsiveProps; if (responsiveKeys.includes(key)) { (displayProps[k as keyof typeof displayProps] as CssResponsiveProps[typeof key]) = v; } else { (otherProps[k as keyof typeof otherProps] as CssResponsiveProps[typeof key]) = v; } }); return [displayProps, otherProps]; } export function isChildElement(parent: Element | null | undefined, child: Element | null | undefined): boolean { if (!parent || !child) { return false; } let node: (Node & ParentNode) | null = child; while (node) { if (node === parent) { return true; } node = node.parentNode; } return false; } export function mapTextChilds( children: ReactNode | undefined, mapMethod: (child: string) => ReactNode ): ReactNode { return React.Children.map(children, (child) => { if (React.isValidElement<{ children: ReactNode }>(child)) { if (child.props.children) { child = React.cloneElement(child, { ...child.props, children: mapTextChilds(child.props.children, mapMethod), }); } return child; } // console.log('mapTextChilds', typeof child); if (typeof child === 'string' && child) { return mapMethod(child); } return child; }); } export function mapChildsByType( children: ReactNode | undefined, type: React.ElementType, mapMethod: (child: ReactNode) => ReactNode ): ReactNode { return React.Children.map(children, (child) => { if (!React.isValidElement<{ children: ReactNode }>(child)) { return child; } // console.log('mapChildsByType', child.type); if (child.type === type) { return mapMethod(child); } else if (child.props.children) { child = React.cloneElement(child, { ...child.props, children: mapChildsByType(child.props.children, type, mapMethod), }); } return child; }); } export function getChildsByType(children: ReactNode | undefined, type: React.ElementType): [ReactNode | undefined, ReactNode | undefined] { const items: ReactNode[] = []; const others = React.Children.map(children, (item) => { if (!React.isValidElement(item)) { return item; } if (item.type === type) { items.push(item); return null; } return item; }); // console.log(items); const childs = items.length > 0 ? items : undefined; return [childs, others]; } export function hasChildOfType(children: ReactNode | undefined, type: React.ElementType): boolean { const [foundChildren, otherChildren] = getChildsByType(children, type); const hasChildOfType = foundChildren !== undefined; return hasChildOfType; } /** * todo verification of above methods */ export function getId() { return Math.random().toString(32).slice(2, 10); } export function capitalize(str: string | symbol | number | undefined | null) { const safeStr = String(str).trim(); return safeStr.charAt(0).toUpperCase() + safeStr.slice(1); } export function hasChild(children: ReactNode, child: ElementType) { const types = React.Children.map(children, item => { if (!React.isValidElement(item)) return null; return item.type; }); return (types || []).includes(child); } /* export function hasType(children: ReactNode, type: ElementType): boolean { let has = false; React.Children.forEach(children, (child: ReactNode) => { if (React.isValidElement(child)) { if (child.type === type) { has = true; } else { if (child.props.children) { has = hasType(child.props.children, type); } } } }); return has; } export function recursiveMapChild(children: ReactNode, mapElement: (child: ReactElement) => ReactNode): ReactNode { return React.Children.map(children, (child: ReactNode) => { if (!React.isValidElement(child)) { return child; } if (child.props.children) { child = React.cloneElement(child, { children: recursiveMapChild(child.props.children, mapElement) }); } return mapElement(child); }); } */ export function mapChild(elementType: ElementType, mapElement: (element: ReactElement) => ReactNode, children: ReactNode | undefined): ReactNode | undefined { return React.Children.map(children, item => { if (!React.isValidElement(item)) { return item; } if (item.type === elementType) { return mapElement(item); } return item; }); } export function pickChild(elementType: ElementType, children: ReactNode | undefined): [ReactNode[] | undefined, ReactNode | undefined] { const pickedChildren: ReactNode[] = []; const unpicked: ReactNode = React.Children.map(children, item => { if (!React.isValidElement(item)) { return item; } if (item.type === elementType) { pickedChildren.push(item); return null; } return item; }); const picked = pickedChildren.length >= 0 ? pickedChildren : undefined; return [picked, unpicked]; } export function pickChildByProps(children: ReactNode | undefined, key: string, value: any): [ReactNode | undefined, ReactNode | undefined] { const target: ReactNode[] = []; const isArray = Array.isArray(value); const withoutPropChildren = React.Children.map(children, item => { if (!React.isValidElement(item)) { return null; } if (!item.props) { return item; } if (isArray) { if (value.includes((item.props as any)[key])) { target.push(item); return null; } return item; } if ((item.props as any)[key] === value) { target.push(item); return null; } return item; }); const targetChildren = target.length >= 0 ? target : undefined; return [withoutPropChildren, targetChildren]; } export function pickChildrenFirst(children: ReactNode | undefined): ReactNode | undefined { return React.Children.toArray(children)[0]; } export function setChildrenProps(children: ReactNode | undefined, props: Record, targetComponents: Array = []): ReactNode | undefined { if (React.Children.count(children) === 0) { return []; } const allowAll = targetComponents.length === 0; const clone = (child: ReactElement, props = {}) => React.cloneElement(child, props); return React.Children.map(children, item => { if (!React.isValidElement(item)) { return item; } if (allowAll) { return clone(item, props); } const isAllowed = targetComponents.find(child => child === item.type); if (isAllowed) { return clone(item, props); } return item; }); } export function setChildrenIndex(children: ReactNode | undefined, targetComponents: Array = []): ReactNode | undefined { if (React.Children.count(children) === 0) { return []; } const allowAll = targetComponents.length === 0; const clone = (child: ReactElement, props = {}) => React.cloneElement(child, props); let index = 0; return React.Children.map(children, item => { if (!React.isValidElement(item)) { return item; } index = index + 1; if (allowAll) { return clone(item, { index }); } const isAllowed = targetComponents.find(child => child === item.type); if (isAllowed) { return clone(item, { index }); } index = index - 1; return item; }); } export function getReactNode(node?: ReactNode | (() => ReactNode)): ReactNode { if (!node) { return null; } if (typeof node !== 'function') { return node; } return (node as () => ReactNode)(); } export function isBrowser(): boolean { return Boolean(typeof window !== 'undefined' && window.document && window.document.createElement); } export function isMac(): boolean { return isBrowser() && navigator.platform.toUpperCase().indexOf('MAC') >= 0; } export function isCSSNumberValue(value?: string | number): boolean { return value !== undefined && !Number.isNaN(+value); } export function isMixerElement(element?: HTMLElement): boolean { if (!element) { return false; } if (element?.dataset && element?.dataset['mixer']) { return true; } element.attributes.getNamedItem('data-mixer'); return !!element.attributes.getNamedItem('data-mixer'); }