All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.core.headers.ts Maven / Gradle / Ivy

The newest version!
import { RowData, Column, Header, HeaderGroup, Table } from '../types'
import { memo } from '../utils'
import { TableFeature } from './table'

export interface CoreHeaderGroup {
  id: string
  depth: number
  headers: Header[]
}

export interface HeaderContext {
  table: Table
  header: Header
  column: Column
}

export interface CoreHeader {
  id: string
  index: number
  depth: number
  column: Column
  headerGroup: HeaderGroup
  subHeaders: Header[]
  colSpan: number
  rowSpan: number
  getLeafHeaders: () => Header[]
  isPlaceholder: boolean
  placeholderId?: string
  getContext: () => HeaderContext
}

export interface HeadersInstance {
  getHeaderGroups: () => HeaderGroup[]
  getLeftHeaderGroups: () => HeaderGroup[]
  getCenterHeaderGroups: () => HeaderGroup[]
  getRightHeaderGroups: () => HeaderGroup[]

  getFooterGroups: () => HeaderGroup[]
  getLeftFooterGroups: () => HeaderGroup[]
  getCenterFooterGroups: () => HeaderGroup[]
  getRightFooterGroups: () => HeaderGroup[]

  getFlatHeaders: () => Header[]
  getLeftFlatHeaders: () => Header[]
  getCenterFlatHeaders: () => Header[]
  getRightFlatHeaders: () => Header[]

  getLeafHeaders: () => Header[]
  getLeftLeafHeaders: () => Header[]
  getCenterLeafHeaders: () => Header[]
  getRightLeafHeaders: () => Header[]
}

//

function createHeader(
  table: Table,
  column: Column,
  options: {
    id?: string
    isPlaceholder?: boolean
    placeholderId?: string
    index: number
    depth: number
  }
): Header {
  const id = options.id ?? column.id

  let header: CoreHeader = {
    id,
    column,
    index: options.index,
    isPlaceholder: !!options.isPlaceholder,
    placeholderId: options.placeholderId,
    depth: options.depth,
    subHeaders: [],
    colSpan: 0,
    rowSpan: 0,
    headerGroup: null!,
    getLeafHeaders: (): Header[] => {
      const leafHeaders: Header[] = []

      const recurseHeader = (h: CoreHeader) => {
        if (h.subHeaders && h.subHeaders.length) {
          h.subHeaders.map(recurseHeader)
        }
        leafHeaders.push(h as Header)
      }

      recurseHeader(header)

      return leafHeaders
    },
    getContext: () => ({
      table,
      header: header as Header,
      column,
    }),
  }

  table._features.forEach(feature => {
    Object.assign(header, feature.createHeader?.(header, table))
  })

  return header as Header
}

export const Headers: TableFeature = {
  createTable: (
    table: Table
  ): HeadersInstance => {
    return {
      // Header Groups

      getHeaderGroups: memo(
        () => [
          table.getAllColumns(),
          table.getVisibleLeafColumns(),
          table.getState().columnPinning.left,
          table.getState().columnPinning.right,
        ],
        (allColumns, leafColumns, left, right) => {
          const leftColumns =
            left
              ?.map(columnId => leafColumns.find(d => d.id === columnId)!)
              .filter(Boolean) ?? []

          const rightColumns =
            right
              ?.map(columnId => leafColumns.find(d => d.id === columnId)!)
              .filter(Boolean) ?? []

          const centerColumns = leafColumns.filter(
            column => !left?.includes(column.id) && !right?.includes(column.id)
          )

          const headerGroups = buildHeaderGroups(
            allColumns,
            [...leftColumns, ...centerColumns, ...rightColumns],
            table
          )

          return headerGroups
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getHeaderGroups',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getCenterHeaderGroups: memo(
        () => [
          table.getAllColumns(),
          table.getVisibleLeafColumns(),
          table.getState().columnPinning.left,
          table.getState().columnPinning.right,
        ],
        (allColumns, leafColumns, left, right) => {
          leafColumns = leafColumns.filter(
            column => !left?.includes(column.id) && !right?.includes(column.id)
          )
          return buildHeaderGroups(allColumns, leafColumns, table, 'center')
        },
        {
          key:
            process.env.NODE_ENV === 'development' && 'getCenterHeaderGroups',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getLeftHeaderGroups: memo(
        () => [
          table.getAllColumns(),
          table.getVisibleLeafColumns(),
          table.getState().columnPinning.left,
        ],
        (allColumns, leafColumns, left) => {
          const orderedLeafColumns =
            left
              ?.map(columnId => leafColumns.find(d => d.id === columnId)!)
              .filter(Boolean) ?? []

          return buildHeaderGroups(
            allColumns,
            orderedLeafColumns,
            table,
            'left'
          )
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getLeftHeaderGroups',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getRightHeaderGroups: memo(
        () => [
          table.getAllColumns(),
          table.getVisibleLeafColumns(),
          table.getState().columnPinning.right,
        ],
        (allColumns, leafColumns, right) => {
          const orderedLeafColumns =
            right
              ?.map(columnId => leafColumns.find(d => d.id === columnId)!)
              .filter(Boolean) ?? []

          return buildHeaderGroups(
            allColumns,
            orderedLeafColumns,
            table,
            'right'
          )
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getRightHeaderGroups',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      // Footer Groups

      getFooterGroups: memo(
        () => [table.getHeaderGroups()],
        headerGroups => {
          return [...headerGroups].reverse()
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getFooterGroups',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getLeftFooterGroups: memo(
        () => [table.getLeftHeaderGroups()],
        headerGroups => {
          return [...headerGroups].reverse()
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getLeftFooterGroups',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getCenterFooterGroups: memo(
        () => [table.getCenterHeaderGroups()],
        headerGroups => {
          return [...headerGroups].reverse()
        },
        {
          key:
            process.env.NODE_ENV === 'development' && 'getCenterFooterGroups',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getRightFooterGroups: memo(
        () => [table.getRightHeaderGroups()],
        headerGroups => {
          return [...headerGroups].reverse()
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getRightFooterGroups',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      // Flat Headers

      getFlatHeaders: memo(
        () => [table.getHeaderGroups()],
        headerGroups => {
          return headerGroups
            .map(headerGroup => {
              return headerGroup.headers
            })
            .flat()
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getFlatHeaders',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getLeftFlatHeaders: memo(
        () => [table.getLeftHeaderGroups()],
        left => {
          return left
            .map(headerGroup => {
              return headerGroup.headers
            })
            .flat()
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getLeftFlatHeaders',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getCenterFlatHeaders: memo(
        () => [table.getCenterHeaderGroups()],
        left => {
          return left
            .map(headerGroup => {
              return headerGroup.headers
            })
            .flat()
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getCenterFlatHeaders',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getRightFlatHeaders: memo(
        () => [table.getRightHeaderGroups()],
        left => {
          return left
            .map(headerGroup => {
              return headerGroup.headers
            })
            .flat()
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getRightFlatHeaders',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      // Leaf Headers

      getCenterLeafHeaders: memo(
        () => [table.getCenterFlatHeaders()],
        flatHeaders => {
          return flatHeaders.filter(header => !header.subHeaders?.length)
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getCenterLeafHeaders',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getLeftLeafHeaders: memo(
        () => [table.getLeftFlatHeaders()],
        flatHeaders => {
          return flatHeaders.filter(header => !header.subHeaders?.length)
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getLeftLeafHeaders',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getRightLeafHeaders: memo(
        () => [table.getRightFlatHeaders()],
        flatHeaders => {
          return flatHeaders.filter(header => !header.subHeaders?.length)
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getRightLeafHeaders',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),

      getLeafHeaders: memo(
        () => [
          table.getLeftHeaderGroups(),
          table.getCenterHeaderGroups(),
          table.getRightHeaderGroups(),
        ],
        (left, center, right) => {
          return [
            ...(left[0]?.headers ?? []),
            ...(center[0]?.headers ?? []),
            ...(right[0]?.headers ?? []),
          ]
            .map(header => {
              return header.getLeafHeaders()
            })
            .flat()
        },
        {
          key: process.env.NODE_ENV === 'development' && 'getLeafHeaders',
          debug: () => table.options.debugAll ?? table.options.debugHeaders,
        }
      ),
    }
  },
}

export function buildHeaderGroups(
  allColumns: Column[],
  columnsToGroup: Column[],
  table: Table,
  headerFamily?: 'center' | 'left' | 'right'
) {
  // Find the max depth of the columns:
  // build the leaf column row
  // build each buffer row going up
  //    placeholder for non-existent level
  //    real column for existing level

  let maxDepth = 0

  const findMaxDepth = (columns: Column[], depth = 1) => {
    maxDepth = Math.max(maxDepth, depth)

    columns
      .filter(column => column.getIsVisible())
      .forEach(column => {
        if (column.columns?.length) {
          findMaxDepth(column.columns, depth + 1)
        }
      }, 0)
  }

  findMaxDepth(allColumns)

  let headerGroups: HeaderGroup[] = []

  const createHeaderGroup = (
    headersToGroup: Header[],
    depth: number
  ) => {
    // The header group we are creating
    const headerGroup: HeaderGroup = {
      depth,
      id: [headerFamily, `${depth}`].filter(Boolean).join('_'),
      headers: [],
    }

    // The parent columns we're going to scan next
    const pendingParentHeaders: Header[] = []

    // Scan each column for parents
    headersToGroup.forEach(headerToGroup => {
      // What is the latest (last) parent column?

      const latestPendingParentHeader = [...pendingParentHeaders].reverse()[0]

      const isLeafHeader = headerToGroup.column.depth === headerGroup.depth

      let column: Column
      let isPlaceholder = false

      if (isLeafHeader && headerToGroup.column.parent) {
        // The parent header is new
        column = headerToGroup.column.parent
      } else {
        // The parent header is repeated
        column = headerToGroup.column
        isPlaceholder = true
      }

      if (
        latestPendingParentHeader &&
        latestPendingParentHeader?.column === column
      ) {
        // This column is repeated. Add it as a sub header to the next batch
        latestPendingParentHeader.subHeaders.push(headerToGroup)
      } else {
        // This is a new header. Let's create it
        const header = createHeader(table, column, {
          id: [headerFamily, depth, column.id, headerToGroup?.id]
            .filter(Boolean)
            .join('_'),
          isPlaceholder,
          placeholderId: isPlaceholder
            ? `${pendingParentHeaders.filter(d => d.column === column).length}`
            : undefined,
          depth,
          index: pendingParentHeaders.length,
        })

        // Add the headerToGroup as a subHeader of the new header
        header.subHeaders.push(headerToGroup)
        // Add the new header to the pendingParentHeaders to get grouped
        // in the next batch
        pendingParentHeaders.push(header)
      }

      headerGroup.headers.push(headerToGroup)
      headerToGroup.headerGroup = headerGroup
    })

    headerGroups.push(headerGroup)

    if (depth > 0) {
      createHeaderGroup(pendingParentHeaders, depth - 1)
    }
  }

  const bottomHeaders = columnsToGroup.map((column, index) =>
    createHeader(table, column, {
      depth: maxDepth,
      index,
    })
  )

  createHeaderGroup(bottomHeaders, maxDepth - 1)

  headerGroups.reverse()

  // headerGroups = headerGroups.filter(headerGroup => {
  //   return !headerGroup.headers.every(header => header.isPlaceholder)
  // })

  const recurseHeadersForSpans = (
    headers: Header[]
  ): { colSpan: number; rowSpan: number }[] => {
    const filteredHeaders = headers.filter(header =>
      header.column.getIsVisible()
    )

    return filteredHeaders.map(header => {
      let colSpan = 0
      let rowSpan = 0
      let childRowSpans = [0]

      if (header.subHeaders && header.subHeaders.length) {
        childRowSpans = []

        recurseHeadersForSpans(header.subHeaders).forEach(
          ({ colSpan: childColSpan, rowSpan: childRowSpan }) => {
            colSpan += childColSpan
            childRowSpans.push(childRowSpan)
          }
        )
      } else {
        colSpan = 1
      }

      const minChildRowSpan = Math.min(...childRowSpans)
      rowSpan = rowSpan + minChildRowSpan

      header.colSpan = colSpan
      header.rowSpan = rowSpan

      return { colSpan, rowSpan }
    })
  }

  recurseHeadersForSpans(headerGroups[0]?.headers ?? [])

  return headerGroups
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy