import React, { useEffect, useState, useRef, useMemo, forwardRef, } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { deepMerge } from '@jiaminghi/charts/lib/util/index'; import { deepClone } from '@jiaminghi/c-render/lib/plugin/util'; import { useAutoResize, co } from '@jiaminghi/data-view-react'; import './style.less'; 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 }) { if (!header.length) { return []; } header = [...header]; if (index) header.unshift(indexHeader); return header; } function calcRows({ data, index, headerBGC, rowNum, }) { if (index) { data = data.map((row, i) => { row = [...row]; const indexTag = `${i + 1}`; row.unshift(indexTag); return row; }); } data = data.map((ceils, i) => ({ ceils, rowIndex: i })); const rowLength = data.length; if (rowLength > rowNum && rowLength < 2 * rowNum) { data = [...data, ...data]; } return data.map((d, i) => ({ ...d, scroll: i })); } function calcAligns(mergedConfig, header) { const columnNum = header.length; const aligns = new Array(columnNum).fill('left'); const { align } = mergedConfig; return deepMerge(aligns, align); } const ScrollBoard = forwardRef(({ onClick, config = {}, className, style, onMouseOver, }, ref) => { const { width, height, domRef } = useAutoResize(ref); const [state, setState] = useState({ mergedConfig: null, header: [], rows: [], rowsShow: [], widths: [], heights: [], aligns: [], }); const { mergedConfig, header, rows, widths, heights, aligns, rowsShow, } = 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 = { widths, heights }; Object.assign(stateRef.current, data); setState((state) => ({ ...state, ...data })); } const [init, setInit] = useState(true); function calcData() { // const mergedConfig = deepMerge( // deepClone(defaultConfig, true), // config || {}, // ); const mergedConfig = { ...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 = { mergedConfig, header, rows, widths, aligns, heights: init ? heights : state.heights.concat(heights), rowsShow: init ? rows : state.rowsShow, }; setInit(false); Object.assign(stateRef.current, data, { rowsData: rows, animationIndex: stateRef.current.animationIndex, }); setState((state) => ({ ...state, ...data })); } function calcWidths({ columnWidth, header }, rowsData) { const usedWidth = columnWidth.reduce((all, w) => 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 }, header) { 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) { 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 = rowsData.slice(animationIndex); rows.push(...rowsData.slice(0, animationIndex)); rows = rows.slice(0, carousel === 'page' ? rowNum * 2 : rowNum + 1); const heights = new Array(rowLength).fill(avgHeight); setState((state) => ({ ...state, rows, heights, rowsShow: rows, })); yield new Promise((resolve) => setTimeout(resolve, 300)); animationIndex += animationNum; const back = animationIndex - rowLength; if (back >= 0) animationIndex = back; const newHeights = [...heights]; newHeights.splice(0, animationNum, ...new Array(animationNum).fill(0)); Object.assign(stateRef.current, { animationIndex }); setState((state) => ({ ...state, heights: newHeights })); } function emitEvent(handle, ri, ci, row, ceil) { const { ceils, rowIndex } = row; handle && handle({ row: ceils, ceil, rowIndex, columnIndex: ci, }); } function handleHover(enter, ri, ci, row, ceil) { if (enter) emitEvent(onMouseOver, ri, ci, row, ceil); if (!mergedConfig.hoverPause) return; const { pause, resume } = task.current; enter && pause && resume ? pause() : resume && resume(); } // updateRows(rows, animationIndex) { // const { mergedConfig, animationHandler, animation } = this // this.mergedConfig = { // ...mergedConfig, // data: [...rows] // } // this.needCalc = true // if (typeof animationIndex === 'number') this.animationIndex = animationIndex // if (!animationHandler) animation(true) // } const getBackgroundColor = (rowIndex) => mergedConfig[rowIndex % 2 === 0 ? 'evenRowBGC' : 'oddRowBGC']; const task = useRef({}); 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) { setState((prestate) => ({ ...prestate, rowsShow: state.rows, })); return; } task.current = co(loop); 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 && (
{rowsShow.map((row, ri) => (
{row.ceils.map((ceil, ci) => { if (typeof (ceil) === 'string') { return (
emitEvent(onClick, ri, ci, row, ceil)} onMouseEnter={() => handleHover(true, ri, ci, row, ceil)} onMouseLeave={() => handleHover(false)} /> ); } return (
handleHover(true, ri, ci, row, ceil)} onMouseLeave={() => handleHover(false)} > {ceil}
); })}
))}
)}
); }); ScrollBoard.propTypes = { config: PropTypes.object, onClick: PropTypes.func, onMouseOver: PropTypes.func, className: PropTypes.string, style: PropTypes.object, }; export default ScrollBoard;