Merge pull request #3378 from influxdata/migrate-to-simple-multi-grid
Use custom multigrid componentpull/10616/head
commit
f4830576d5
|
@ -16,6 +16,7 @@
|
||||||
1. [#3245](https://github.com/influxdata/chronograf/pull/3245): Display 'no results' on cells without results
|
1. [#3245](https://github.com/influxdata/chronograf/pull/3245): Display 'no results' on cells without results
|
||||||
1. [#3354](https://github.com/influxdata/chronograf/pull/3354): Disable template variables for non editing users
|
1. [#3354](https://github.com/influxdata/chronograf/pull/3354): Disable template variables for non editing users
|
||||||
1. [#3353](https://github.com/influxdata/chronograf/pull/3353): YAxisLabels in Dashboard Graph Builder not showing until graph is redrawn
|
1. [#3353](https://github.com/influxdata/chronograf/pull/3353): YAxisLabels in Dashboard Graph Builder not showing until graph is redrawn
|
||||||
|
1. [#3378](https://github.com/influxdata/chronograf/pull/3378): Ensure table graphs have a consistent ux between chrome and firefox
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,64 @@
|
||||||
|
import _ from 'lodash'
|
||||||
import React, {Component} from 'react'
|
import React, {Component} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import {Scrollbars} from 'react-custom-scrollbars'
|
import {Scrollbars} from 'react-custom-scrollbars'
|
||||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||||
|
|
||||||
@ErrorHandling
|
interface DefaultProps {
|
||||||
class FancyScrollbar extends Component {
|
autoHide: boolean
|
||||||
constructor(props) {
|
autoHeight: boolean
|
||||||
super(props)
|
maxHeight: number
|
||||||
}
|
setScrollTop: (value: React.MouseEvent<JSX.Element>) => void
|
||||||
|
style: React.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
interface Props {
|
||||||
|
className?: string
|
||||||
|
scrollTop?: number
|
||||||
|
scrollLeft?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
@ErrorHandling
|
||||||
|
class FancyScrollbar extends Component<Props & Partial<DefaultProps>> {
|
||||||
|
public static defaultProps = {
|
||||||
autoHide: true,
|
autoHide: true,
|
||||||
autoHeight: false,
|
autoHeight: false,
|
||||||
|
maxHeight: null,
|
||||||
|
style: {},
|
||||||
setScrollTop: () => {},
|
setScrollTop: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMakeDiv = className => props => {
|
private ref: React.RefObject<Scrollbars>
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.ref = React.createRef<Scrollbars>()
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateScroll() {
|
||||||
|
const ref = this.ref.current
|
||||||
|
if (ref && !_.isNil(this.props.scrollTop)) {
|
||||||
|
ref.scrollTop(this.props.scrollTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref && !_.isNil(this.props.scrollLeft)) {
|
||||||
|
ref.scrollLeft(this.props.scrollLeft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.updateScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate() {
|
||||||
|
this.updateScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleMakeDiv = (className: string) => (props): JSX.Element => {
|
||||||
return <div {...props} className={`fancy-scroll--${className}`} />
|
return <div {...props} className={`fancy-scroll--${className}`} />
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
autoHide,
|
autoHide,
|
||||||
autoHeight,
|
autoHeight,
|
||||||
|
@ -28,6 +66,7 @@ class FancyScrollbar extends Component {
|
||||||
className,
|
className,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
setScrollTop,
|
setScrollTop,
|
||||||
|
style,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -35,6 +74,8 @@ class FancyScrollbar extends Component {
|
||||||
className={classnames('fancy-scroll--container', {
|
className={classnames('fancy-scroll--container', {
|
||||||
[className]: className,
|
[className]: className,
|
||||||
})}
|
})}
|
||||||
|
ref={this.ref}
|
||||||
|
style={style}
|
||||||
onScroll={setScrollTop}
|
onScroll={setScrollTop}
|
||||||
autoHide={autoHide}
|
autoHide={autoHide}
|
||||||
autoHideTimeout={1000}
|
autoHideTimeout={1000}
|
||||||
|
@ -53,15 +94,4 @@ class FancyScrollbar extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {bool, func, node, number, string} = PropTypes
|
|
||||||
|
|
||||||
FancyScrollbar.propTypes = {
|
|
||||||
children: node.isRequired,
|
|
||||||
className: string,
|
|
||||||
autoHide: bool,
|
|
||||||
autoHeight: bool,
|
|
||||||
maxHeight: number,
|
|
||||||
setScrollTop: func,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FancyScrollbar
|
export default FancyScrollbar
|
|
@ -0,0 +1,105 @@
|
||||||
|
import {CellMeasurerCache} from 'react-virtualized'
|
||||||
|
|
||||||
|
interface CellMeasurerCacheDecoratorParams {
|
||||||
|
cellMeasurerCache: CellMeasurerCache
|
||||||
|
columnIndexOffset: number
|
||||||
|
rowIndexOffset: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndexParam {
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class CellMeasurerCacheDecorator {
|
||||||
|
private cellMeasurerCache: CellMeasurerCache
|
||||||
|
private columnIndexOffset: number
|
||||||
|
private rowIndexOffset: number
|
||||||
|
|
||||||
|
constructor(params: Partial<CellMeasurerCacheDecoratorParams> = {}) {
|
||||||
|
const {
|
||||||
|
cellMeasurerCache,
|
||||||
|
columnIndexOffset = 0,
|
||||||
|
rowIndexOffset = 0,
|
||||||
|
} = params
|
||||||
|
|
||||||
|
this.cellMeasurerCache = cellMeasurerCache
|
||||||
|
this.columnIndexOffset = columnIndexOffset
|
||||||
|
this.rowIndexOffset = rowIndexOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(rowIndex: number, columnIndex: number): void {
|
||||||
|
this.cellMeasurerCache.clear(
|
||||||
|
rowIndex + this.rowIndexOffset,
|
||||||
|
columnIndex + this.columnIndexOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearAll(): void {
|
||||||
|
this.cellMeasurerCache.clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
public columnWidth = ({index}: IndexParam) => {
|
||||||
|
this.cellMeasurerCache.columnWidth({
|
||||||
|
index: index + this.columnIndexOffset,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultHeight(): number {
|
||||||
|
return this.cellMeasurerCache.defaultHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultWidth(): number {
|
||||||
|
return this.cellMeasurerCache.defaultWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasFixedHeight(): boolean {
|
||||||
|
return this.cellMeasurerCache.hasFixedHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasFixedWidth(): boolean {
|
||||||
|
return this.cellMeasurerCache.hasFixedWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHeight(rowIndex: number, columnIndex: number = 0): number | null {
|
||||||
|
return this.cellMeasurerCache.getHeight(
|
||||||
|
rowIndex + this.rowIndexOffset,
|
||||||
|
columnIndex + this.columnIndexOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getWidth(rowIndex: number, columnIndex: number = 0): number | null {
|
||||||
|
return this.cellMeasurerCache.getWidth(
|
||||||
|
rowIndex + this.rowIndexOffset,
|
||||||
|
columnIndex + this.columnIndexOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public has(rowIndex: number, columnIndex: number = 0): boolean {
|
||||||
|
return this.cellMeasurerCache.has(
|
||||||
|
rowIndex + this.rowIndexOffset,
|
||||||
|
columnIndex + this.columnIndexOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public rowHeight = ({index}: IndexParam) => {
|
||||||
|
this.cellMeasurerCache.rowHeight({
|
||||||
|
index: index + this.rowIndexOffset,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public set(
|
||||||
|
rowIndex: number,
|
||||||
|
columnIndex: number,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): void {
|
||||||
|
this.cellMeasurerCache.set(
|
||||||
|
rowIndex + this.rowIndexOffset,
|
||||||
|
columnIndex + this.columnIndexOffset,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CellMeasurerCacheDecorator
|
|
@ -0,0 +1,813 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import CellMeasurerCacheDecorator from './CellMeasurerCacheDecorator'
|
||||||
|
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||||
|
import {Grid} from 'react-virtualized'
|
||||||
|
|
||||||
|
const SCROLLBAR_SIZE_BUFFER = 20
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
columnCount?: number
|
||||||
|
classNameBottomLeftGrid?: string
|
||||||
|
classNameBottomRightGrid?: string
|
||||||
|
classNameTopLeftGrid?: string
|
||||||
|
classNameTopRightGrid?: string
|
||||||
|
enableFixedColumnScroll?: boolean
|
||||||
|
enableFixedRowScroll?: boolean
|
||||||
|
fixedColumnCount?: number
|
||||||
|
fixedRowCount?: number
|
||||||
|
style?: object
|
||||||
|
styleBottomLeftGrid?: object
|
||||||
|
styleBottomRightGrid?: object
|
||||||
|
styleTopLeftGrid?: object
|
||||||
|
styleTopRightGrid?: object
|
||||||
|
scrollTop?: number
|
||||||
|
scrollLeft?: number
|
||||||
|
rowCount?: number
|
||||||
|
rowHeight?: (arg: {index: number}) => {} | number
|
||||||
|
columnWidth?: (arg: object) => {} | number
|
||||||
|
onScroll?: (arg: object) => {}
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
scrollToRow?: () => {}
|
||||||
|
onSectionRendered?: () => {}
|
||||||
|
scrollToColumn?: () => {}
|
||||||
|
cellRenderer?: (arg: object) => JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
scrollLeft: number
|
||||||
|
scrollTop: number
|
||||||
|
scrollbarSize: number
|
||||||
|
showHorizontalScrollbar: boolean
|
||||||
|
showVerticalScrollbar: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders 1, 2, or 4 Grids depending on configuration.
|
||||||
|
* A main (body) Grid will always be rendered.
|
||||||
|
* Optionally, 1-2 Grids for sticky header rows will also be rendered.
|
||||||
|
* If no sticky columns, only 1 sticky header Grid will be rendered.
|
||||||
|
* If sticky columns, 2 sticky header Grids will be rendered.
|
||||||
|
*/
|
||||||
|
class MultiGrid extends React.PureComponent<Props, State> {
|
||||||
|
public static defaultProps = {
|
||||||
|
classNameBottomLeftGrid: '',
|
||||||
|
classNameBottomRightGrid: '',
|
||||||
|
classNameTopLeftGrid: '',
|
||||||
|
classNameTopRightGrid: '',
|
||||||
|
enableFixedColumnScroll: false,
|
||||||
|
enableFixedRowScroll: false,
|
||||||
|
fixedColumnCount: 0,
|
||||||
|
fixedRowCount: 0,
|
||||||
|
scrollToColumn: -1,
|
||||||
|
scrollToRow: -1,
|
||||||
|
style: {},
|
||||||
|
styleBottomLeftGrid: {},
|
||||||
|
styleBottomRightGrid: {},
|
||||||
|
styleTopLeftGrid: {},
|
||||||
|
styleTopRightGrid: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getDerivedStateFromProps(nextProps, prevState) {
|
||||||
|
if (
|
||||||
|
nextProps.scrollLeft !== prevState.scrollLeft ||
|
||||||
|
nextProps.scrollTop !== prevState.scrollTop
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
scrollLeft:
|
||||||
|
nextProps.scrollLeft != null && nextProps.scrollLeft >= 0
|
||||||
|
? nextProps.scrollLeft
|
||||||
|
: prevState.scrollLeft,
|
||||||
|
scrollTop:
|
||||||
|
nextProps.scrollTop != null && nextProps.scrollTop >= 0
|
||||||
|
? nextProps.scrollTop
|
||||||
|
: prevState.scrollTop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private deferredInvalidateColumnIndex: number = 0
|
||||||
|
private deferredInvalidateRowIndex: number = 0
|
||||||
|
private bottomLeftGrid: Grid
|
||||||
|
private bottomRightGrid: Grid
|
||||||
|
private topLeftGrid: Grid
|
||||||
|
private topRightGrid: Grid
|
||||||
|
private deferredMeasurementCacheBottomLeftGrid: CellMeasurerCacheDecorator
|
||||||
|
private deferredMeasurementCacheBottomRightGrid: CellMeasurerCacheDecorator
|
||||||
|
private deferredMeasurementCacheTopRightGrid: CellMeasurerCacheDecorator
|
||||||
|
private leftGridWidth: number | null = 0
|
||||||
|
private topGridHeight: number | null = 0
|
||||||
|
private lastRenderedColumnWidth: (arg: object) => {} | number
|
||||||
|
private lastRenderedFixedColumnCount: number = 0
|
||||||
|
private lastRenderedFixedRowCount: number = 0
|
||||||
|
private lastRenderedRowHeight: (arg: {index: number}) => {} | number
|
||||||
|
private bottomRightGridStyle: object | null
|
||||||
|
private topRightGridStyle: object | null
|
||||||
|
private lastRenderedStyle: object | null
|
||||||
|
private lastRenderedHeight: number = 0
|
||||||
|
private lastRenderedWidth: number = 0
|
||||||
|
private containerTopStyle: object | null
|
||||||
|
private containerBottomStyle: object | null
|
||||||
|
private containerOuterStyle: object | null
|
||||||
|
private lastRenderedStyleBottomLeftGrid: object | null
|
||||||
|
private lastRenderedStyleBottomRightGrid: object | null
|
||||||
|
private lastRenderedStyleTopLeftGrid: object | null
|
||||||
|
private lastRenderedStyleTopRightGrid: object | null
|
||||||
|
private bottomLeftGridStyle: object | null
|
||||||
|
private topLeftGridStyle: object | null
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
scrollLeft: 0,
|
||||||
|
scrollTop: 0,
|
||||||
|
scrollbarSize: 0,
|
||||||
|
showHorizontalScrollbar: false,
|
||||||
|
showVerticalScrollbar: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const {deferredMeasurementCache, fixedColumnCount, fixedRowCount} = props
|
||||||
|
|
||||||
|
this.maybeCalculateCachedStyles(true)
|
||||||
|
|
||||||
|
if (deferredMeasurementCache) {
|
||||||
|
this.deferredMeasurementCacheBottomLeftGrid =
|
||||||
|
fixedRowCount > 0
|
||||||
|
? new CellMeasurerCacheDecorator({
|
||||||
|
cellMeasurerCache: deferredMeasurementCache,
|
||||||
|
columnIndexOffset: 0,
|
||||||
|
rowIndexOffset: fixedRowCount,
|
||||||
|
})
|
||||||
|
: deferredMeasurementCache
|
||||||
|
|
||||||
|
this.deferredMeasurementCacheBottomRightGrid =
|
||||||
|
fixedColumnCount > 0 || fixedRowCount > 0
|
||||||
|
? new CellMeasurerCacheDecorator({
|
||||||
|
cellMeasurerCache: deferredMeasurementCache,
|
||||||
|
columnIndexOffset: fixedColumnCount,
|
||||||
|
rowIndexOffset: fixedRowCount,
|
||||||
|
})
|
||||||
|
: deferredMeasurementCache
|
||||||
|
|
||||||
|
this.deferredMeasurementCacheTopRightGrid =
|
||||||
|
fixedColumnCount > 0
|
||||||
|
? new CellMeasurerCacheDecorator({
|
||||||
|
cellMeasurerCache: deferredMeasurementCache,
|
||||||
|
columnIndexOffset: fixedColumnCount,
|
||||||
|
rowIndexOffset: 0,
|
||||||
|
})
|
||||||
|
: deferredMeasurementCache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public forceUpdateGrids() {
|
||||||
|
if (this.bottomLeftGrid) {
|
||||||
|
this.bottomLeftGrid.forceUpdate()
|
||||||
|
}
|
||||||
|
if (this.bottomRightGrid) {
|
||||||
|
this.bottomRightGrid.forceUpdate()
|
||||||
|
}
|
||||||
|
if (this.topLeftGrid) {
|
||||||
|
this.topLeftGrid.forceUpdate()
|
||||||
|
}
|
||||||
|
if (this.topRightGrid) {
|
||||||
|
this.topRightGrid.forceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See Grid#invalidateCellSizeAfterRender */
|
||||||
|
public invalidateCellSizeAfterRender({columnIndex = 0, rowIndex = 0} = {}) {
|
||||||
|
this.deferredInvalidateColumnIndex =
|
||||||
|
typeof this.deferredInvalidateColumnIndex === 'number'
|
||||||
|
? Math.min(this.deferredInvalidateColumnIndex, columnIndex)
|
||||||
|
: columnIndex
|
||||||
|
this.deferredInvalidateRowIndex =
|
||||||
|
typeof this.deferredInvalidateRowIndex === 'number'
|
||||||
|
? Math.min(this.deferredInvalidateRowIndex, rowIndex)
|
||||||
|
: rowIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See Grid#measureAllCells */
|
||||||
|
public measureAllCells() {
|
||||||
|
if (this.bottomLeftGrid) {
|
||||||
|
this.bottomLeftGrid.measureAllCells()
|
||||||
|
}
|
||||||
|
if (this.bottomRightGrid) {
|
||||||
|
this.bottomRightGrid.measureAllCells()
|
||||||
|
}
|
||||||
|
if (this.topLeftGrid) {
|
||||||
|
this.topLeftGrid.measureAllCells()
|
||||||
|
}
|
||||||
|
if (this.topRightGrid) {
|
||||||
|
this.topRightGrid.measureAllCells()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public recomputeGridSize({columnIndex = 0, rowIndex = 0} = {}) {
|
||||||
|
const {fixedColumnCount, fixedRowCount} = this.props
|
||||||
|
|
||||||
|
const adjustedColumnIndex = Math.max(0, columnIndex - fixedColumnCount)
|
||||||
|
const adjustedRowIndex = Math.max(0, rowIndex - fixedRowCount)
|
||||||
|
|
||||||
|
if (this.bottomLeftGrid) {
|
||||||
|
this.bottomLeftGrid.recomputeGridSize({
|
||||||
|
columnIndex,
|
||||||
|
rowIndex: adjustedRowIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.bottomRightGrid) {
|
||||||
|
this.bottomRightGrid.recomputeGridSize({
|
||||||
|
columnIndex: adjustedColumnIndex,
|
||||||
|
rowIndex: adjustedRowIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.topLeftGrid) {
|
||||||
|
this.topLeftGrid.recomputeGridSize({
|
||||||
|
columnIndex,
|
||||||
|
rowIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.topRightGrid) {
|
||||||
|
this.topRightGrid.recomputeGridSize({
|
||||||
|
columnIndex: adjustedColumnIndex,
|
||||||
|
rowIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.leftGridWidth = null
|
||||||
|
this.topGridHeight = null
|
||||||
|
this.maybeCalculateCachedStyles(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
const {scrollLeft, scrollTop} = this.props
|
||||||
|
|
||||||
|
if (scrollLeft > 0 || scrollTop > 0) {
|
||||||
|
const newState: Partial<State> = {}
|
||||||
|
|
||||||
|
if (scrollLeft > 0) {
|
||||||
|
newState.scrollLeft = scrollLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollTop > 0) {
|
||||||
|
newState.scrollTop = scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({...this.state, ...newState})
|
||||||
|
}
|
||||||
|
this.handleInvalidatedGridSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate() {
|
||||||
|
this.handleInvalidatedGridSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {
|
||||||
|
onScroll,
|
||||||
|
scrollLeft: scrollLeftProp, // eslint-disable-line no-unused-vars
|
||||||
|
onSectionRendered,
|
||||||
|
scrollToRow,
|
||||||
|
scrollToColumn,
|
||||||
|
scrollTop: scrollTopProp, // eslint-disable-line no-unused-vars
|
||||||
|
...rest
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
this.prepareForRender()
|
||||||
|
|
||||||
|
// Don't render any of our Grids if there are no cells.
|
||||||
|
// This mirrors what Grid does,
|
||||||
|
// And prevents us from recording inaccurage measurements when used with CellMeasurer.
|
||||||
|
if (this.props.width === 0 || this.props.height === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// scrollTop and scrollLeft props are explicitly filtered out and ignored
|
||||||
|
|
||||||
|
const {scrollLeft, scrollTop} = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={this.containerOuterStyle}>
|
||||||
|
<div style={this.containerTopStyle}>
|
||||||
|
{this.renderTopLeftGrid(rest)}
|
||||||
|
{this.renderTopRightGrid({
|
||||||
|
...rest,
|
||||||
|
...onScroll,
|
||||||
|
scrollLeft,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div style={this.containerBottomStyle}>
|
||||||
|
{this.renderBottomLeftGrid({
|
||||||
|
...rest,
|
||||||
|
onScroll,
|
||||||
|
scrollTop,
|
||||||
|
})}
|
||||||
|
{this.renderBottomRightGrid({
|
||||||
|
...rest,
|
||||||
|
onScroll,
|
||||||
|
onSectionRendered,
|
||||||
|
scrollLeft,
|
||||||
|
scrollToColumn,
|
||||||
|
scrollToRow,
|
||||||
|
scrollTop,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public cellRendererBottomLeftGrid = ({
|
||||||
|
rowIndex,
|
||||||
|
...rest
|
||||||
|
}: Partial<Props> & {rowIndex: number; key: string}): JSX.Element => {
|
||||||
|
const {cellRenderer, fixedRowCount, rowCount} = this.props
|
||||||
|
|
||||||
|
if (rowIndex === rowCount - fixedRowCount) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={rest.key}
|
||||||
|
style={{
|
||||||
|
...rest.style,
|
||||||
|
height: SCROLLBAR_SIZE_BUFFER,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return cellRenderer({
|
||||||
|
...rest,
|
||||||
|
parent: this,
|
||||||
|
rowIndex: rowIndex + fixedRowCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBottomGridHeight(props) {
|
||||||
|
const {height} = props
|
||||||
|
|
||||||
|
const topGridHeight = this.getTopGridHeight(props)
|
||||||
|
|
||||||
|
return height - topGridHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLeftGridWidth(props) {
|
||||||
|
const {fixedColumnCount, columnWidth} = props
|
||||||
|
|
||||||
|
if (this.leftGridWidth == null) {
|
||||||
|
if (typeof columnWidth === 'function') {
|
||||||
|
let leftGridWidth = 0
|
||||||
|
|
||||||
|
for (let index = 0; index < fixedColumnCount; index++) {
|
||||||
|
leftGridWidth += columnWidth({index})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.leftGridWidth = leftGridWidth
|
||||||
|
} else {
|
||||||
|
this.leftGridWidth = columnWidth * fixedColumnCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.leftGridWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRightGridWidth(props) {
|
||||||
|
const {width} = props
|
||||||
|
|
||||||
|
const leftGridWidth = this.getLeftGridWidth(props)
|
||||||
|
const result = width - leftGridWidth
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTopGridHeight(props) {
|
||||||
|
const {fixedRowCount, rowHeight} = props
|
||||||
|
|
||||||
|
if (this.topGridHeight == null) {
|
||||||
|
if (typeof rowHeight === 'function') {
|
||||||
|
let topGridHeight = 0
|
||||||
|
|
||||||
|
for (let index = 0; index < fixedRowCount; index++) {
|
||||||
|
topGridHeight += rowHeight({index})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.topGridHeight = topGridHeight
|
||||||
|
} else {
|
||||||
|
this.topGridHeight = rowHeight * fixedRowCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.topGridHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
private onScrollbarsScroll = (e: React.MouseEvent<JSX.Element>) => {
|
||||||
|
const {target} = e
|
||||||
|
this.onScroll(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onScroll = scrollInfo => {
|
||||||
|
const {scrollLeft, scrollTop} = scrollInfo
|
||||||
|
this.setState({
|
||||||
|
scrollLeft,
|
||||||
|
scrollTop,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {onScroll} = this.props
|
||||||
|
if (onScroll) {
|
||||||
|
onScroll(scrollInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onScrollLeft = scrollInfo => {
|
||||||
|
const {scrollLeft} = scrollInfo
|
||||||
|
this.onScroll({
|
||||||
|
scrollLeft,
|
||||||
|
scrollTop: this.state.scrollTop,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderBottomLeftGrid(props) {
|
||||||
|
const {fixedColumnCount, fixedRowCount, rowCount} = props
|
||||||
|
|
||||||
|
if (!fixedColumnCount) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = this.getLeftGridWidth(props)
|
||||||
|
const height = this.getBottomGridHeight(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
{...props}
|
||||||
|
cellRenderer={this.cellRendererBottomLeftGrid}
|
||||||
|
className={this.props.classNameBottomLeftGrid}
|
||||||
|
columnCount={fixedColumnCount}
|
||||||
|
deferredMeasurementCache={this.deferredMeasurementCacheBottomLeftGrid}
|
||||||
|
onScroll={this.onScroll}
|
||||||
|
height={height}
|
||||||
|
ref={this.bottomLeftGridRef}
|
||||||
|
rowCount={Math.max(0, rowCount - fixedRowCount)}
|
||||||
|
rowHeight={this.rowHeightBottomGrid}
|
||||||
|
style={{
|
||||||
|
...this.bottomLeftGridStyle,
|
||||||
|
}}
|
||||||
|
tabIndex={null}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderBottomRightGrid(props) {
|
||||||
|
const {
|
||||||
|
columnCount,
|
||||||
|
fixedColumnCount,
|
||||||
|
fixedRowCount,
|
||||||
|
rowCount,
|
||||||
|
scrollToColumn,
|
||||||
|
scrollToRow,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const width = this.getRightGridWidth(props)
|
||||||
|
const height = this.getBottomGridHeight(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FancyScrollbar
|
||||||
|
style={{...this.bottomRightGridStyle, width, height}}
|
||||||
|
autoHide={true}
|
||||||
|
scrollTop={this.state.scrollTop}
|
||||||
|
scrollLeft={this.state.scrollLeft}
|
||||||
|
setScrollTop={this.onScrollbarsScroll}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
{...props}
|
||||||
|
cellRenderer={this.cellRendererBottomRightGrid}
|
||||||
|
className={this.props.classNameBottomRightGrid}
|
||||||
|
columnCount={Math.max(0, columnCount - fixedColumnCount)}
|
||||||
|
columnWidth={this.columnWidthRightGrid}
|
||||||
|
deferredMeasurementCache={
|
||||||
|
this.deferredMeasurementCacheBottomRightGrid
|
||||||
|
}
|
||||||
|
height={height}
|
||||||
|
ref={this.bottomRightGridRef}
|
||||||
|
rowCount={Math.max(0, rowCount - fixedRowCount)}
|
||||||
|
rowHeight={this.rowHeightBottomGrid}
|
||||||
|
onScroll={this.onScroll}
|
||||||
|
scrollToColumn={scrollToColumn - fixedColumnCount}
|
||||||
|
scrollToRow={scrollToRow - fixedRowCount}
|
||||||
|
style={{
|
||||||
|
...this.bottomRightGridStyle,
|
||||||
|
overflowX: false,
|
||||||
|
overflowY: true,
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
</FancyScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTopLeftGrid(props) {
|
||||||
|
const {fixedColumnCount, fixedRowCount} = props
|
||||||
|
|
||||||
|
if (!fixedColumnCount || !fixedRowCount) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
{...props}
|
||||||
|
className={this.props.classNameTopLeftGrid}
|
||||||
|
columnCount={fixedColumnCount}
|
||||||
|
height={this.getTopGridHeight(props)}
|
||||||
|
ref={this.topLeftGridRef}
|
||||||
|
rowCount={fixedRowCount}
|
||||||
|
style={this.topLeftGridStyle}
|
||||||
|
tabIndex={null}
|
||||||
|
width={this.getLeftGridWidth(props)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTopRightGrid(props) {
|
||||||
|
const {
|
||||||
|
columnCount,
|
||||||
|
enableFixedRowScroll,
|
||||||
|
fixedColumnCount,
|
||||||
|
fixedRowCount,
|
||||||
|
scrollLeft,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
if (!fixedRowCount) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = this.getRightGridWidth(props)
|
||||||
|
const height = this.getTopGridHeight(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
{...props}
|
||||||
|
cellRenderer={this.cellRendererTopRightGrid}
|
||||||
|
className={this.props.classNameTopRightGrid}
|
||||||
|
columnCount={Math.max(0, columnCount - fixedColumnCount)}
|
||||||
|
columnWidth={this.columnWidthRightGrid}
|
||||||
|
deferredMeasurementCache={this.deferredMeasurementCacheTopRightGrid}
|
||||||
|
height={height}
|
||||||
|
onScroll={enableFixedRowScroll ? this.onScrollLeft : undefined}
|
||||||
|
ref={this.topRightGridRef}
|
||||||
|
rowCount={fixedRowCount}
|
||||||
|
scrollLeft={scrollLeft}
|
||||||
|
style={this.topRightGridStyle}
|
||||||
|
tabIndex={null}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private rowHeightBottomGrid = ({index}) => {
|
||||||
|
const {fixedRowCount, rowCount, rowHeight} = this.props
|
||||||
|
const {scrollbarSize, showVerticalScrollbar} = this.state
|
||||||
|
|
||||||
|
// An extra cell is added to the count
|
||||||
|
// This gives the smaller Grid extra room for offset,
|
||||||
|
// In case the main (bottom right) Grid has a scrollbar
|
||||||
|
// If no scrollbar, the extra space is overflow:hidden anyway
|
||||||
|
if (showVerticalScrollbar && index === rowCount - fixedRowCount) {
|
||||||
|
return scrollbarSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof rowHeight === 'function'
|
||||||
|
? rowHeight({index: index + fixedRowCount})
|
||||||
|
: rowHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
private topLeftGridRef = ref => {
|
||||||
|
this.topLeftGrid = ref
|
||||||
|
}
|
||||||
|
|
||||||
|
private topRightGridRef = ref => {
|
||||||
|
this.topRightGrid = ref
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Avoid recreating inline styles each render; this bypasses Grid's shallowCompare.
|
||||||
|
* This method recalculates styles only when specific props change.
|
||||||
|
*/
|
||||||
|
private maybeCalculateCachedStyles(resetAll) {
|
||||||
|
const {
|
||||||
|
columnWidth,
|
||||||
|
height,
|
||||||
|
fixedColumnCount,
|
||||||
|
fixedRowCount,
|
||||||
|
rowHeight,
|
||||||
|
style,
|
||||||
|
styleBottomLeftGrid,
|
||||||
|
styleBottomRightGrid,
|
||||||
|
styleTopLeftGrid,
|
||||||
|
styleTopRightGrid,
|
||||||
|
width,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const sizeChange =
|
||||||
|
resetAll ||
|
||||||
|
height !== this.lastRenderedHeight ||
|
||||||
|
width !== this.lastRenderedWidth
|
||||||
|
const leftSizeChange =
|
||||||
|
resetAll ||
|
||||||
|
columnWidth !== this.lastRenderedColumnWidth ||
|
||||||
|
fixedColumnCount !== this.lastRenderedFixedColumnCount
|
||||||
|
const topSizeChange =
|
||||||
|
resetAll ||
|
||||||
|
fixedRowCount !== this.lastRenderedFixedRowCount ||
|
||||||
|
rowHeight !== this.lastRenderedRowHeight
|
||||||
|
|
||||||
|
if (resetAll || sizeChange || style !== this.lastRenderedStyle) {
|
||||||
|
this.containerOuterStyle = {
|
||||||
|
height,
|
||||||
|
overflow: 'visible', // Let :focus outline show through
|
||||||
|
width,
|
||||||
|
...style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetAll || sizeChange || topSizeChange) {
|
||||||
|
this.containerTopStyle = {
|
||||||
|
height: this.getTopGridHeight(this.props),
|
||||||
|
position: 'relative',
|
||||||
|
width,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.containerBottomStyle = {
|
||||||
|
height: height - this.getTopGridHeight(this.props),
|
||||||
|
overflow: 'visible', // Let :focus outline show through
|
||||||
|
position: 'relative',
|
||||||
|
width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
resetAll ||
|
||||||
|
styleBottomLeftGrid !== this.lastRenderedStyleBottomLeftGrid
|
||||||
|
) {
|
||||||
|
this.bottomLeftGridStyle = {
|
||||||
|
left: 0,
|
||||||
|
overflowY: 'hidden',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
...styleBottomLeftGrid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
resetAll ||
|
||||||
|
leftSizeChange ||
|
||||||
|
styleBottomRightGrid !== this.lastRenderedStyleBottomRightGrid
|
||||||
|
) {
|
||||||
|
this.bottomRightGridStyle = {
|
||||||
|
left: this.getLeftGridWidth(this.props),
|
||||||
|
position: 'absolute',
|
||||||
|
...styleBottomRightGrid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetAll || styleTopLeftGrid !== this.lastRenderedStyleTopLeftGrid) {
|
||||||
|
this.topLeftGridStyle = {
|
||||||
|
left: 0,
|
||||||
|
overflowX: 'hidden',
|
||||||
|
overflowY: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
...styleTopLeftGrid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
resetAll ||
|
||||||
|
leftSizeChange ||
|
||||||
|
styleTopRightGrid !== this.lastRenderedStyleTopRightGrid
|
||||||
|
) {
|
||||||
|
this.topRightGridStyle = {
|
||||||
|
left: this.getLeftGridWidth(this.props),
|
||||||
|
overflowX: 'hidden',
|
||||||
|
overflowY: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
...styleTopRightGrid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastRenderedColumnWidth = columnWidth
|
||||||
|
this.lastRenderedFixedColumnCount = fixedColumnCount
|
||||||
|
this.lastRenderedFixedRowCount = fixedRowCount
|
||||||
|
this.lastRenderedHeight = height
|
||||||
|
this.lastRenderedRowHeight = rowHeight
|
||||||
|
this.lastRenderedStyle = style
|
||||||
|
this.lastRenderedStyleBottomLeftGrid = styleBottomLeftGrid
|
||||||
|
this.lastRenderedStyleBottomRightGrid = styleBottomRightGrid
|
||||||
|
this.lastRenderedStyleTopLeftGrid = styleTopLeftGrid
|
||||||
|
this.lastRenderedStyleTopRightGrid = styleTopRightGrid
|
||||||
|
this.lastRenderedWidth = width
|
||||||
|
}
|
||||||
|
|
||||||
|
private bottomLeftGridRef = ref => {
|
||||||
|
this.bottomLeftGrid = ref
|
||||||
|
}
|
||||||
|
|
||||||
|
private bottomRightGridRef = ref => {
|
||||||
|
this.bottomRightGrid = ref
|
||||||
|
}
|
||||||
|
|
||||||
|
private cellRendererBottomRightGrid = ({columnIndex, rowIndex, ...rest}) => {
|
||||||
|
const {cellRenderer, fixedColumnCount, fixedRowCount} = this.props
|
||||||
|
|
||||||
|
return cellRenderer({
|
||||||
|
...rest,
|
||||||
|
columnIndex: columnIndex + fixedColumnCount,
|
||||||
|
parent: this,
|
||||||
|
rowIndex: rowIndex + fixedRowCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private cellRendererTopRightGrid = ({columnIndex, ...rest}) => {
|
||||||
|
const {cellRenderer, columnCount, fixedColumnCount} = this.props
|
||||||
|
|
||||||
|
if (columnIndex === columnCount - fixedColumnCount) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={rest.key}
|
||||||
|
style={{
|
||||||
|
...rest.style,
|
||||||
|
width: SCROLLBAR_SIZE_BUFFER,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return cellRenderer({
|
||||||
|
...rest,
|
||||||
|
columnIndex: columnIndex + fixedColumnCount,
|
||||||
|
parent: this,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private columnWidthRightGrid = ({index}) => {
|
||||||
|
const {columnCount, fixedColumnCount, columnWidth} = this.props
|
||||||
|
const {scrollbarSize, showHorizontalScrollbar} = this.state
|
||||||
|
|
||||||
|
// An extra cell is added to the count
|
||||||
|
// This gives the smaller Grid extra room for offset,
|
||||||
|
// In case the main (bottom right) Grid has a scrollbar
|
||||||
|
// If no scrollbar, the extra space is overflow:hidden anyway
|
||||||
|
if (showHorizontalScrollbar && index === columnCount - fixedColumnCount) {
|
||||||
|
return scrollbarSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof columnWidth === 'function'
|
||||||
|
? columnWidth({index: index + fixedColumnCount})
|
||||||
|
: columnWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleInvalidatedGridSize() {
|
||||||
|
if (typeof this.deferredInvalidateColumnIndex === 'number') {
|
||||||
|
const columnIndex = this.deferredInvalidateColumnIndex
|
||||||
|
const rowIndex = this.deferredInvalidateRowIndex
|
||||||
|
|
||||||
|
this.deferredInvalidateColumnIndex = null
|
||||||
|
this.deferredInvalidateRowIndex = null
|
||||||
|
|
||||||
|
this.recomputeGridSize({
|
||||||
|
columnIndex,
|
||||||
|
rowIndex,
|
||||||
|
})
|
||||||
|
this.forceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private prepareForRender() {
|
||||||
|
if (
|
||||||
|
this.lastRenderedColumnWidth !== this.props.columnWidth ||
|
||||||
|
this.lastRenderedFixedColumnCount !== this.props.fixedColumnCount
|
||||||
|
) {
|
||||||
|
this.leftGridWidth = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.lastRenderedFixedRowCount !== this.props.fixedRowCount ||
|
||||||
|
this.lastRenderedRowHeight !== this.props.rowHeight
|
||||||
|
) {
|
||||||
|
this.topGridHeight = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.maybeCalculateCachedStyles(false)
|
||||||
|
|
||||||
|
this.lastRenderedColumnWidth = this.props.columnWidth
|
||||||
|
this.lastRenderedFixedColumnCount = this.props.fixedColumnCount
|
||||||
|
this.lastRenderedFixedRowCount = this.props.fixedRowCount
|
||||||
|
this.lastRenderedRowHeight = this.props.rowHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MultiGrid
|
|
@ -0,0 +1,2 @@
|
||||||
|
import MultiGrid from './MultiGrid'
|
||||||
|
export {MultiGrid}
|
|
@ -4,7 +4,8 @@ import _ from 'lodash'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
|
|
||||||
import {MultiGrid, ColumnSizer} from 'react-virtualized'
|
import {ColumnSizer} from 'react-virtualized'
|
||||||
|
import {MultiGrid} from 'src/shared/components/MultiGrid'
|
||||||
import {bindActionCreators} from 'redux'
|
import {bindActionCreators} from 'redux'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import {reduce} from 'fast.js'
|
import {reduce} from 'fast.js'
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
// Highlight
|
// Highlight
|
||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -80,8 +80,8 @@
|
||||||
padding-right: 17px;
|
padding-right: 17px;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
font-family: 'icomoon';
|
font-family: "icomoon";
|
||||||
content: '\e902';
|
content: "\e902";
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|
Loading…
Reference in New Issue