/** * @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;
标签:MAP,style,辅助,函数,classList,骨架,return,const,string From: https://www.cnblogs.com/cheng-up/p/17005251.html