import React, { useEffect, useState, useRef, useMemo, forwardRef } from "react"; import useAutoResize from "./use/autoResize"; import { deepMerge } from "./util"; import { deepClone } from "./util/utils"; import { co } from "./util"; import classnames from "classnames"; import "./index.css"; import { current } from "@reduxjs/toolkit"; interface ScrollBoardProps { config?: object; onClick?: () => void; onMouseOver?: () => void; className?: string; style?: object; } interface TaskType { end: () => void; pause: () => void; resume: () => void; } const defaultConfig = { /** * @description Board header * @type {Array} * @default header = [] * @example header = ['column1', 'column2', 'column3'] */ header: [], /** * @description Board data * @type {Array} * @default data = [] */ data: [], /** * @description Row num * @type {Number} * @default rowNum = 5 */ rowNum: 5, /** * @description Header background color * @type {String} * @default headerBGC = '#00BAFF' */ headerBGC: "#00BAFF", /** * @description Odd row background color * @type {String} * @default oddRowBGC = '#003B51' */ oddRowBGC: "#003B51", /** * @description Even row background color * @type {String} * @default evenRowBGC = '#003B51' */ evenRowBGC: "#0A2732", /** * @description Scroll wait time * @type {Number} * @default waitTime = 2000 */ waitTime: 2000, /** * @description Header height * @type {Number} * @default headerHeight = 35 */ headerHeight: 35, /** * @description Column width * @type {Array} * @default columnWidth = [] */ columnWidth: [], /** * @description Column align * @type {Array} * @default align = [] * @example align = ['left', 'center', 'right'] */ align: [], /** * @description Show index * @type {Boolean} * @default index = false */ index: false, /** * @description index Header * @type {String} * @default indexHeader = '#' */ indexHeader: "#", /** * @description Carousel type * @type {String} * @default carousel = 'single' * @example carousel = 'single' | 'page' */ carousel: "single", /** * @description Pause scroll when mouse hovered * @type {Boolean} * @default hoverPause = true * @example hoverPause = true | false */ hoverPause: true, }; function calcHeaderData({ header, index, indexHeader }: any) { if (!header.length) { return []; } header = [...header]; if (index) header.unshift(indexHeader); return header; } function calcRows({ data, index, headerBGC, rowNum }: any) { if (index) { data = data.map((row: any, i: any) => { row = [...row]; const indexTag = `${ i + 1 }`; row.unshift(indexTag); return row; }); } data = data.map((ceils: any, i: any) => ({ ceils, rowIndex: i })); const rowLength = data.length; if (rowLength > rowNum && rowLength < 2 * rowNum) { data = [...data, ...data]; } return data.map((d: any, i: any) => ({ ...d, scroll: i })); } function calcAligns(mergedConfig: any, header: any) { const columnNum = header.length; let aligns = new Array(columnNum).fill("left"); const { align } = mergedConfig; return deepMerge(aligns, align); } const ScrollBoard = forwardRef( ( { config, onClick, onMouseOver, className, style }: ScrollBoardProps, ref ) => { const { width, height, domRef } = useAutoResize(ref); const [state, setState] = useState({ mergedConfig: { align: [], carousel: "", columnWidth: [], data: [], evenRowBGC: "", header: [], headerBGC: "", headerHeight: 35, hoverPause: true, index: false, oddRowBGC: "", indexHeader: "#", rowNum: 5, waitTime: 2000, }, header: [], rows: [], widths: [], heights: [], aligns: [], }); const { mergedConfig, header, rows, widths, heights, aligns } = state; const stateRef = useRef({ ...state, rowsData: [], avgHeight: 0, animationIndex: 0, }); Object.assign(stateRef.current, state); function onResize() { if (!mergedConfig) return; const widths = calcWidths(mergedConfig, stateRef.current.rowsData); const heights = calcHeights(mergedConfig, header); const data: any = { widths, heights }; Object.assign(stateRef.current, data); setState((state) => ({ ...state, ...data })); } function calcData() { const mergedConfig = deepMerge( // deepClone(defaultConfig, true), deepClone(defaultConfig), config || {} ); const header = calcHeaderData(mergedConfig); const rows = calcRows(mergedConfig); const widths = calcWidths(mergedConfig, stateRef.current.rowsData); const heights = calcHeights(mergedConfig, header); const aligns = calcAligns(mergedConfig, header); const data: any = { mergedConfig, header, rows, widths, aligns, heights, }; Object.assign(stateRef.current, data, { rowsData: rows, animationIndex: 0, }); setState((state) => ({ ...state, ...data })); } function calcWidths({ columnWidth, header }: any, rowsData: any) { const usedWidth = columnWidth.reduce((all: any, w: any) => all + w, 0); let columnNum = 0; if (rowsData[0]) { columnNum = rowsData[0].ceils.length; } else if (header.length) { columnNum = header.length; } const avgWidth = (width - usedWidth) / (columnNum - columnWidth.length); const widths = new Array(columnNum).fill(avgWidth); return deepMerge(widths, columnWidth); } function calcHeights({ headerHeight, rowNum, data }: any, header: any) { let allHeight = height; if (header.length) allHeight -= headerHeight; const avgHeight = allHeight / rowNum; Object.assign(stateRef.current, { avgHeight }); return new Array(data.length).fill(avgHeight); } function* animation( start = false ): Generator, void, unknown> { let { avgHeight, animationIndex, mergedConfig: { waitTime, carousel, rowNum }, rowsData, } = stateRef.current; const rowLength = rowsData.length; if (start) yield new Promise((resolve) => setTimeout(resolve, waitTime)); const animationNum = carousel === "single" ? 1 : rowNum; let rows: any = rowsData.slice(animationIndex); rows.push(...rowsData.slice(0, animationIndex)); rows = rows.slice(0, carousel === "page" ? rowNum * 2 : rowNum + 1); const heights: any = new Array(rowLength).fill(avgHeight); setState((state) => ({ ...state, rows, heights })); yield new Promise((resolve) => setTimeout(resolve, 300)); animationIndex += animationNum; const back = animationIndex - rowLength; if (back >= 0) animationIndex = back; const newHeights: any = [...heights]; newHeights.splice(0, animationNum, ...new Array(animationNum).fill(0)); Object.assign(stateRef.current, { animationIndex }); setState((state) => ({ ...state, heights: newHeights })); } function emitEvent(handle: any, ri: any, ci: any, row: any, ceil: any) { const { ceils, rowIndex } = row; handle && handle({ row: ceils, ceil, rowIndex, columnIndex: ci }); } function handleHover( enter: any, ri?: any, ci?: any, row?: any, ceil?: any ) { if (enter) emitEvent(onMouseOver, ri, ci, row, ceil); if (!mergedConfig.hoverPause) return; if (task.current) { const { pause, resume } = task.current; enter ? (function () { if (pause) pause(); })() : (function () { if (resume) resume(); })(); } } const getBackgroundColor = (rowIndex: any) => mergedConfig[rowIndex % 2 === 0 ? "evenRowBGC" : "oddRowBGC"]; const task = useRef(null); useEffect(() => { calcData(); let start = true; function* loop() { while (true) { yield* animation(start); start = false; const { waitTime } = stateRef.current.mergedConfig; yield new Promise((resolve) => setTimeout(resolve, waitTime - 300)); } } const { mergedConfig: { rowNum }, rows: rowsData, } = stateRef.current; const rowLength = rowsData.length; if (rowNum >= rowLength) return; // @ts-ignore task.current = co(loop); if (task.current) { return task.current.end; } }, [config, domRef.current]); useEffect(onResize, [width, height, domRef.current]); const classNames = useMemo( () => classnames("dv-scroll-board", className), [className] ); return (
{!!header.length && !!mergedConfig && (
{header.map((headerItem, i) => (
))}
)} {!!mergedConfig && (
{rows.map((row: any, ri) => (
{row.ceils.map((ceil: any, ci: any) => (
emitEvent(onClick, ri, ci, row, ceil)} onMouseEnter={() => handleHover(true, ri, ci, row, ceil)} onMouseLeave={() => handleHover(false)} /> ))}
))}
)}
); } ); export default ScrollBoard;