@tdb/util
Version:
Shared helpers and utilities.
402 lines (371 loc) • 9.98 kB
text/typescript
import { R, React } from '../../libs';
import { value as valueUtil } from '../value';
import {
IFormatCss,
IImageOptions,
IBackgroundImageStyles,
Falsy,
GlamorValue,
} from './types';
import { css as glamorCss } from 'glamor';
import { arrayToEdges } from './util';
export * from './util';
export const MEDIA_QUERY_RETINA = `@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)`;
/**
* Constructs a style object for an image.
*
* For turning image files (PNG/JPG/SVG) into data-uri's see:
* https://github.com/webpack/url-loader
*
* @param {string} image1x: The normal image resolution (base64 encoded)
* @param {string} image2x: The retina image resolution (base64 encoded)
* @param {integer} width: Optional. The width of the image.
* @param {integer} height: Optional. The height of the image.
*/
export const image = (
image1x: string | undefined,
image2x: string | undefined,
options: IImageOptions = { width: 10, height: 10 },
): IBackgroundImageStyles => {
// Prepare image based on current screen density.
if (!image1x) {
throw new Error('Must have at least a 1x image.');
}
const { width, height } = options;
const result: any = {
width,
height,
backgroundImage: `url(${image1x})`,
backgroundSize: `${width}px ${height}px`,
backgroundRepeat: 'no-repeat',
};
if (image2x) {
result[MEDIA_QUERY_RETINA] = {
backgroundImage: `url(${image2x})`,
};
}
// Finish up.
return result;
};
const mergeAndReplace = (key: string, value: any, target: any) => {
Object.assign(target, value);
delete target[key];
return target;
};
const formatImage = (
key: string,
value: Array<string | number | undefined>,
target: any,
) => {
// Wrangle parameters.
let [image1x, image2x, width, height] = value; // tslint:disable-line
if (R.is(Number, image2x)) {
height = width;
width = image2x;
image2x = undefined;
}
const options = {
width: width as number,
height: height as number,
};
const style = image(image1x as string, image2x as string, options);
mergeAndReplace(key, style, target);
};
export const toPositionEdges = (
key: string,
value: any = undefined,
):
| {
position: string;
top: number | string | undefined;
right: number | string | undefined;
bottom: number | string | undefined;
left: number | string | undefined;
}
| undefined => {
const edges = arrayToEdges(value);
if (!edges) {
return undefined;
}
const { left, top, right, bottom } = edges;
if (
top === undefined &&
right === undefined &&
bottom === undefined &&
left === undefined
) {
return undefined;
}
return {
position: key.toLowerCase(),
top,
right,
bottom,
left,
};
};
export const formatPositionEdges = (key: string, target: any) => {
const styles = toPositionEdges(key, target[key]);
mergeAndReplace(key, styles, target);
};
/**
* AbsoluteCenter
* - x
* - y
* - xy
*/
const formatAbsoluteCenter = (
key: string,
value: string | boolean | number,
target: any,
) => {
if (value === true) {
value = 'xy';
}
if (value === false || value === undefined || value === null) {
return;
}
const styles = {
position: 'absolute',
left: target.left,
top: target.top,
transform: '',
};
const stringValue = value
.toString()
.trim()
.toLowerCase();
if (stringValue.includes('x')) {
styles.left = '50%';
}
if (stringValue.includes('y')) {
styles.top = '50%';
}
let transform: string;
switch (value) {
case 'yx':
case 'xy':
transform = 'translate(-50%, -50%)';
break;
case 'x':
transform = 'translateX(-50%)';
break;
case 'y':
transform = 'translateY(-50%)';
break;
default:
throw new Error(`AbsoluteCenter value '${value}' not supported.`);
}
styles.transform = `${target.transform || ''} ${transform}`.trim();
mergeAndReplace(key, styles, target);
};
/**
* Spacing on the X:Y plane.
*/
function formatSpacingPlane(
plane: 'x' | 'y' | 'xy',
prefix: 'margin' | 'padding',
key: string,
value: any,
target: any,
) {
const styles = {};
const edges = arrayToEdges(value);
if (edges && plane.includes('x')) {
styles[`${prefix}Left`] = edges.left;
styles[`${prefix}Right`] = edges.right;
}
if (edges && plane.includes('y')) {
styles[`${prefix}Top`] = edges.top;
styles[`${prefix}Bottom`] = edges.bottom;
}
mergeAndReplace(key, styles, target);
}
/**
* Sets up vertical scrolling including iOS momentum scrolling.
* See:
* https://css-tricks.com/snippets/css/momentum-scrolling-on-ios-overflow-elements/
*/
function formatScroll(key: string, value: any, target: any) {
if (value === true) {
const styles = {
overflowX: 'hidden',
overflowY: 'scroll',
WebkitOverflowScrolling: 'touch',
};
mergeAndReplace(key, styles, target);
}
if (value === false) {
const styles = {
overflow: 'hidden',
};
mergeAndReplace(key, styles, target);
}
}
// --------------------------------------------------
const AlignMap: { [k: string]: string } = {
center: 'center',
left: 'flex-start',
top: 'flex-start',
start: 'flex-start',
right: 'flex-end',
bottom: 'flex-end',
end: 'flex-end',
full: 'stretch',
stretch: 'stretch',
baseline: 'baseline',
};
function convertCrossAlignToFlex(token: string): string | undefined {
return AlignMap[token] || undefined; // undefined if not recognised;
}
const MainAlignMap: { [k: string]: string } = {
center: 'center',
left: 'flex-start',
top: 'flex-start',
start: 'flex-start',
right: 'flex-end',
bottom: 'flex-end',
end: 'flex-end',
spaceBetween: 'space-between',
spaceAround: 'space-around',
spaceEvenly: 'space-evenly',
};
function convertMainAlignToFlex(token: string): string | undefined {
return MainAlignMap[token] || undefined; // undefined if not recognised;
}
/**
* Format a flex css helper
* Format: [<direction>]-<crossAlignment>-<mainAlignment>
*/
function formatFlexPosition(
key: string,
value: string,
target: React.CSSProperties,
) {
let direction: 'row' | 'column' | undefined; // Assume horizontal
let mainAlignment: string | undefined;
let crossAlignment: string | undefined;
// Tokenize string
const tokens: string[] = value.split('-').map(token => token.trim());
tokens.map(token => {
const tokenIsOneOf = (options: string[]) => options.includes(token);
if (direction == null && tokenIsOneOf(['horizontal', 'vertical'])) {
direction = token === 'vertical' ? 'column' : 'row'; // tslint:disable-line
return;
}
if (
tokenIsOneOf([
'center',
'start',
'end',
'left',
'right',
'top',
'bottom',
'full',
'baseline',
])
) {
if (crossAlignment == null) {
if (direction == null && tokenIsOneOf(['left', 'right'])) {
direction = 'column';
}
if (direction == null && tokenIsOneOf(['top', 'bottom'])) {
direction = 'row';
}
crossAlignment = convertCrossAlignToFlex(token);
return;
}
mainAlignment = convertMainAlignToFlex(token);
return;
}
if (tokenIsOneOf(['spaceAround', 'spaceBetween', 'spaceEvenly'])) {
mainAlignment = convertMainAlignToFlex(token);
return;
}
});
const styles = {
display: 'flex',
flexDirection: direction,
alignItems: crossAlignment,
justifyContent: mainAlignment,
};
mergeAndReplace(key, styles, target);
}
export const transformStyle = (
style: React.CSSProperties | GlamorValue | Falsy = {},
): React.CSSProperties | GlamorValue => {
if (style == null) {
return {};
}
if (style === false) {
return {};
}
if (!R.is(Object, style)) {
return style;
}
Object.keys(style).forEach(key => {
const value = style[key];
if (value === false || R.isNil(value)) {
delete style[key];
} else if (valueUtil.isPlainObject(value)) {
// NB: This is not using formatCss, as we only want the transform, we don't want to convert it to a glamor value.
style[key] = transformStyle(value); // <== RECURSION.
} else {
switch (key) {
case 'Image':
formatImage(key, value, style);
break;
case 'Absolute':
formatPositionEdges(key, style);
break;
case 'Fixed':
formatPositionEdges(key, style);
break;
case 'AbsoluteCenter':
formatAbsoluteCenter(key, value, style);
break;
case 'Margin':
formatSpacingPlane('xy', 'margin', key, value, style);
break;
case 'MarginX':
formatSpacingPlane('x', 'margin', key, value, style);
break;
case 'MarginY':
formatSpacingPlane('y', 'margin', key, value, style);
break;
case 'Padding':
formatSpacingPlane('xy', 'padding', key, value, style);
break;
case 'PaddingX':
formatSpacingPlane('x', 'padding', key, value, style);
break;
case 'PaddingY':
formatSpacingPlane('y', 'padding', key, value, style);
break;
case 'Flex':
formatFlexPosition(key, value, style);
break;
case 'Scroll':
formatScroll(key, value, style);
break;
default:
// Ignore.
}
}
});
return style;
};
/**
* Helpers for constructing a CSS object.
* NB: This doesn't *actually* return React.CSSProperties, but
*/
const formatCss = (
...styles: Array<React.CSSProperties | GlamorValue | Falsy>
): GlamorValue => {
const newStyles = styles.map(transformStyle);
// Finish up.
return glamorCss(...newStyles) as {};
};
(formatCss as any).image = image;
export const format = formatCss as IFormatCss;