骨架屏生成辅助函数
2022-12-26 10:59 前端小白的江湖路 阅读(125) 评论(0) 编辑 收藏 举报/**
* @description: A simple function to help you generate skeleton
* Demo:
* const s = new Skeleton();
const rootNode = document.querySelector('#root') as HTMLElement;
const classList = [ // give className in which you wanna generate
'module-icon',
'module-title',
'module-input',
'module-btn-others-title-content',
'module-btn-others-btn',
'module-btn',
];
const { html, style } = s.traverse({
rootNode,
classList,
animation: "wave"
});
s.appendTo(html, style);
*
*/
const ANIMATION_TYPE_MAP = {
WAVE: "wave",
NONE: "none"
} as const;
type AnimationValueType = typeof ANIMATION_TYPE_MAP[keyof typeof ANIMATION_TYPE_MAP];
interface SkeletonOptionsType {
rootNode: HTMLElement,
classList: Array<string>,
animation?: AnimationValueType, // default is wave
isFilterOutsideElements?: boolean; // Whether to filter elements outside the screen, default is true
borderRadiusFit?: "auto" | "fit-content"; // auto means if border-radius is less than width/2, use minBorderRadius, default is auto
minBorderRadius?: number; // min border radius, default is 3
}
interface StyleType {
position: "absolute",
left: string;
top: string;
width: string;
height: string;
background: string;
borderRadius: string;
overflow: "hidden"
}
const DIRECTION = {
HORIZONTAL: "HORIZONTAL",
VERTICAL: "VERTICAL",
} as const;
type DirectionType = keyof typeof DIRECTION;
const KEYFRAMES_MAP = {
WAVE: `@keyframes kf-wave {
0% {
transform: translateX(-100%);
}
50% {
transform: translateX(100%);
}
100% {
transform: translateX(100%);
}
}`
};
const ANIMATION_STYLE_MAP = {
WAVE: `animation: kf-wave 1.6s linear 0.5s infinite;
background: linear-gradient( 90deg, transparent, rgba(0, 0, 0, 0.04), transparent );
content: '';
position: absolute;
transform: translateX(-100%);
bottom: 0;
left: 0;
right: 0;
top: 0;`
}
const BORDER_RADIUS_MAP = {
AUTO: "auto",
FIT_CONTENT: "fit-content",
}
class Skeleton {
traverse(skeletonOptions: SkeletonOptionsType) {
const {
rootNode,
classList, animation = ANIMATION_TYPE_MAP.WAVE,
borderRadiusFit=BORDER_RADIUS_MAP.AUTO,
minBorderRadius = 3,
isFilterOutsideElements=true
} = skeletonOptions;
const targetClassMap = this.generateClassMap(classList);
const backTrack = (node) => {
const children = node.children ?? [];
const htmlList: Array<string> = [];
let styleList: Array<string>= [];
for (let child of children) {
const { x, y, width, height, } = child.getBoundingClientRect();
let style: StyleType | {} = {};
if (this.isClassIntersect(child.classList, targetClassMap)) {
if(isFilterOutsideElements && !this.isElementInView(x, y)) {
continue;
}
style = {
position: "absolute",
left: this.transformToPercent(x, DIRECTION.HORIZONTAL),
top: this.transformToPercent(y, DIRECTION.VERTICAL),
width: this.transformToPercent(width, DIRECTION.HORIZONTAL),
height: this.transformToPercent(height, DIRECTION.VERTICAL),
background: "#F1F1F1",
borderRadius: this.getBorderRadius(window.getComputedStyle(child)["border-radius"], borderRadiusFit, minBorderRadius, width),
overflow: "hidden",
}
const className = child.classList[0] + "-skeleton";
style = this.generateStyle(style);
styleList.push(`.${className} {${style}}`);
if(ANIMATION_TYPE_MAP.NONE !== animation) {
styleList.push(`.${className}::after {${ANIMATION_STYLE_MAP[animation.toUpperCase()]}}`)
}
htmlList.push(`<div class=${className}>`);
const childTarget = backTrack(child);
htmlList.push(childTarget.htmlList.join(""));
styleList.push(childTarget.styleList.join(""));
htmlList.push("</div>");
} else {
const childTarget = backTrack(child);
htmlList.push(childTarget.htmlList.join(""));
styleList.push(childTarget.styleList.join(""));
}
}
return {
htmlList,
styleList,
};
}
const { htmlList, styleList } = backTrack(rootNode);
let animationStyle = "";
if(animation === ANIMATION_TYPE_MAP.WAVE) {
animationStyle = `${KEYFRAMES_MAP.WAVE}`;
}
return {
html: `<div className="i-skeleton">${htmlList.join("")}</div>`,
style: animationStyle + styleList.join("")
}
}
appendTo (html: string, style: string) :void {
console.log(html);
console.log(style)
const styleElement = document.createElement("style");
styleElement.innerHTML = style;
document.head.appendChild(styleElement);
document.body.innerHTML = html;
}
getBorderRadius (currentBorderRadius: string | number, borderRadiusMod: string, minBorderRadius: number, width: number) :string {
currentBorderRadius = Number.parseInt(currentBorderRadius as string);
let borderRadius = minBorderRadius;
switch (borderRadiusMod) {
case BORDER_RADIUS_MAP.AUTO:
if(currentBorderRadius < width / 2 - 1) {
borderRadius = minBorderRadius;
}
break;
case BORDER_RADIUS_MAP.FIT_CONTENT:
borderRadius = currentBorderRadius;
break;
default:
break;
}
return `${borderRadius}px`
}
generateClassMap(classList: Array<string>) :Map<string, boolean> {
const map = new Map();
for (let item of classList) {
map.set(item, true);
}
return map;
}
isClassIntersect(classList: Array<string>, targetMap: Map<string, boolean>) {
for (let item of classList) {
if (targetMap.has(item)) {
return true;
}
}
return false;
}
isElementInView (x: number, y: number) :boolean {
if(x > 0 &&
x < document.documentElement.clientWidth &&
y > 0 &&
y < document.documentElement.clientHeight) {
return true;
}
return false;
}
generateStyle(style): string {
let result = "";
for (let [key, value] of Object.entries(style)) {
const keyWithMiddleLine = this.camelToMiddleLine(key);
result += `${keyWithMiddleLine}: ${value};`;
}
return result;
}
camelToMiddleLine(str: string): string {
const p = /\B([A-Z])/g;
return str.replace(p, '-$1').toLowerCase()
}
transformToPercent(distance: number, direction: DirectionType): string {
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
switch (direction) {
case DIRECTION.HORIZONTAL:
return (distance / viewportWidth * 100).toFixed(1) + "%"
break;
default:
return (distance / viewportHeight * 100).toFixed(1) + "%"
}
}
}
export default Skeleton;