├── .eslintrc.yml ├── .gitignore ├── .travis.yml ├── README.md ├── examples.js ├── index.html ├── package.json ├── rollup.config.js ├── src ├── DefaultColumn.js ├── DefaultRow.js ├── Grid.js ├── Header.js ├── List.js ├── List.test.js ├── ResizeGhost.js ├── actionCreators.js ├── examples │ ├── App.css │ ├── App.js │ ├── Code.js │ ├── Page.css │ ├── Page.js │ ├── demos │ │ ├── GridDemo.js │ │ ├── Minimal.js │ │ └── PinnedColumns.js │ ├── index.js │ └── nav.js ├── hoc │ ├── draggable.js │ ├── index.js │ ├── withDefaults.js │ ├── withDefaults.test.js │ ├── withMiddleState.js │ ├── withPinnedColumns.js │ ├── withPinnedColumns.test.js │ └── withScrollProps.js ├── index.js ├── reducers │ ├── columns.js │ ├── columns.test.js │ ├── gridState.js │ ├── gridState.test.js │ ├── index.js │ └── index.test.js └── utils │ ├── bisectColumns.js │ ├── bisectColumns.test.js │ ├── checkProps.js │ ├── checkProps.test.js │ ├── compose.js │ ├── compose.test.js │ ├── findColumn.js │ ├── findColumn.test.js │ ├── getKeysByIndex.js │ ├── getKeysByIndex.test.js │ ├── getVisibleRows.js │ ├── getVisibleRows.test.js │ ├── index.js │ ├── testUtils.js │ ├── transform.js │ ├── transform.test.js │ ├── trimColumnWidth.js │ └── trimColumnWidth.test.js ├── webpack.config.js └── yarn.lock /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 6 3 | sourceType: 'module' 4 | ecmaFeatures: 5 | impliedStrict: true 6 | jsx: true 7 | 8 | plugins: 9 | - 'react' 10 | 11 | env: 12 | browser: true 13 | 14 | rules: 15 | no-unused-vars: 'error' 16 | react/jsx-uses-react: 'error' 17 | react/jsx-uses-vars: 'error' 18 | 19 | no-undef: 'error' 20 | react/react-in-jsx-scope: 'error' 21 | react/jsx-no-undef: 'error' 22 | 23 | indent: ['error', 4, { 24 | SwitchCase: 1 25 | }] 26 | semi: 'error' 27 | no-extra-semi: 'error' 28 | no-unreachable: 'error' 29 | eqeqeq: 'error' 30 | no-var: 'error' 31 | prefer-const: 'error' 32 | object-shorthand: 'error' 33 | constructor-super: 'error' 34 | no-this-before-super: 'error' 35 | no-const-assign: 'error' 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | tmp 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | matrix: 3 | include: 4 | - node_js: 6 5 | cache: yarn 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # datagrid [![Build Status][travis-img]][travis] 2 | 3 | [travis-img]: https://travis-ci.org/TrySound/datagrid.svg 4 | [travis]: https://travis-ci.org/TrySound/datagrid 5 | 6 | Declarative reactive table with unidirectional data flow 7 | 8 | ## API 9 | 10 | ### Grid component 11 | 12 | **data** 13 | 14 | **columns** 15 | 16 | ```js 17 | [ 18 | { 19 | name: string, 20 | displayName: string, 21 | minWidth: number, 22 | width: number || string, // 120, '60%' depends on viewportWidth 23 | maxWidth: number, 24 | pinnedLeft: boolean, 25 | pinnedRight: boolean, 26 | enableResizing: boolean, 27 | enableMoving: boolean, 28 | enableFiltering: boolean, 29 | enableSorting: boolean, 30 | sort: null | 'asc' | 'desc', 31 | placeholder: string, 32 | filter: string 33 | } 34 | ] 35 | ``` 36 | 37 | **gridState** 38 | 39 | ``` 40 | // predefined behaviors 41 | { 42 | moving?: { 43 | name: string, 44 | left: string, 45 | right: string 46 | }, 47 | selectedIndex?: number, 48 | pager?: { 49 | page: number, 50 | size: number 51 | } 52 | } 53 | ``` 54 | 55 | **callback** `(action: Action) => void` 56 | 57 | **scrollTop** `number` 58 | 59 | **viewportWidth** `number` 60 | 61 | **viewportHeight** `number` 62 | 63 | **headerHeight** `number` 64 | 65 | **rowHeight** `number` 66 | 67 | **columnComponent** 68 | 69 | ```js 70 | ({ 71 | gridState, 72 | callback, 73 | column, 74 | index: number, // columnIndex 75 | last: boolean, // is the last column 76 | ghost: boolean // is column moving ghost 77 | }) => nodes 78 | ``` 79 | 80 | **rowComponent** 81 | 82 | ```js 83 | ({ 84 | gridState, 85 | columns, // columns with width 86 | callback, 87 | datum, // row data 88 | index: number // row index 89 | }) => nodes 90 | ``` 91 | 92 | ### reducer 93 | 94 | Coming soon... 95 | 96 | ### selectGridData 97 | 98 | Coming soon... 99 | 100 | ### withScrollProps 101 | 102 | Coming soon... 103 | 104 | ## License 105 | 106 | MIT © [Bogdan Chadkin](mailto:trysound@yandex.ru) 107 | -------------------------------------------------------------------------------- /examples.js: -------------------------------------------------------------------------------- 1 | var _Mathpow=Math.pow,_StringfromCharCode=String.fromCharCode,_Mathmin=Math.min,_Mathfloor=Math.floor,_Mathmax=Math.max,_Mathabs=Math.abs;(function(r){function h(E){if(_[E])return _[E].exports;var w=_[E]={i:E,l:!1,exports:{}};return r[E].call(w.exports,w,w.exports,h),w.l=!0,w.exports}var _={};return h.m=r,h.c=_,h.i=function(E){return E},h.d=function(E,w,S){h.o(E,w)||Object.defineProperty(E,w,{configurable:!1,enumerable:!0,get:S})},h.n=function(E){var w=E&&E.__esModule?function(){return E['default']}:function(){return E};return h.d(w,'a',w),w},h.o=function(E,w){return Object.prototype.hasOwnProperty.call(E,w)},h.p='',h(h.s=74)})([function(r,h,_){r.exports=_(60),r.exports.default=r.exports},function(r,h,_){r.exports=_(59),r.exports.default=r.exports},function(r,h,_){'use strict';var E=_(43);_.d(h,'b',function(){return E.a});var w=_(42);_.d(h,'c',function(){return w.a});var S=_(44);_.d(h,'e',function(){return S.a});var C=_(45);_.d(h,'d',function(){return C.a});var R=_(41);_.d(h,'g',function(){return R.a});var N=_(14);_.d(h,'f',function(){return N.a});var O=_(47);_.d(h,'h',function(){return O.a});var I=_(46);_.d(h,'a',function(){return I.a})},function(r,h,_){'use strict';var E=_(30);_.d(h,'f',function(){return E.a});var w=_(32),S=_(33);_.d(h,'b',function(){return S.a});var C=_(13),R=_(6);_.d(h,'e',function(){return R.a});var N=_(34);_.d(h,'g',function(){return N.a});var O=_(36);_.d(h,'a',function(){return O.a});var I=_(29);_.d(h,'h',function(){return I.a});var M=_(31);_.d(h,'c',function(){return M.a});var A=_(35);_.d(h,'d',function(){return A.a})},function(r,h,_){r.exports=_(62),r.exports.default=r.exports},function(r,h){'use strict';h.e=(O,I,M)=>({type:'MARK_MOVE_DEST',name:O,left:I,right:M});h.f=(O,I,M)=>({type:'MOVE_COLUMN',name:O,left:I,right:M});h.d=(O,I)=>({type:'RESIZE_COLUMN',name:O,size:I});h.c=(O,I)=>({type:'FILTER_COLUMN',name:O,filter:I});h.b=(O)=>({type:'SORT_COLUMN',name:O});h.a=(O)=>({type:'SELECT_ROW',rowIndex:O})},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(1),C=_.n(S),R=_(2);h.a=(N,O)=>(I)=>class extends C.a{constructor(M){super(M),this.state=O(M),this.checkProps='function'==typeof N?N:_.i(R.c)(...N)}componentWillReceiveProps(M){this.checkProps(this.props,M)&&this.setState(O(M))}render(M,A){return w()(I,Object.assign({},M,A))}}},function(r,h,_){'use strict';var E=_(21);_.d(h,'b',function(){return E.a});var w=_(11),S=_(12),C=_(39);_.d(h,'c',function(){return C.a});var R=_(2);_.d(h,'d',function(){return R.a});var N=_(3);_.d(h,'a',function(){return N.a})},function(r,h,_){(function(E){function w(C,R){var N=C[1]||'',O=C[3];if(!O)return N;if(R){var I=S(O),M=O.sources.map(function(A){return'/*# sourceURL='+O.sourceRoot+A+' */'});return[N].concat(M).concat([I]).join('\n')}return[N].join('\n')}function S(C){var R=new E(JSON.stringify(C)).toString('base64');return'/*# '+('sourceMappingURL=data:application/json;charset=utf-8;base64,'+R)+' */'}r.exports=function(C){var R=[];return R.toString=function(){return this.map(function(O){var I=w(O,C);return O[2]?'@media '+O[2]+'{'+I+'}':I}).join('')},R.i=function(N,O){'string'==typeof N&&(N=[[null,N,'']]);for(var I={},M=0,A;M({position:'relative',height:'inherit',boxSizing:'border-box',padding:'0 8px',borderTop:R,borderBottom:R,borderLeft:U&&R||(A.moving&&A.moving.right===P.name?N:R)||'',borderRight:U&&R||T&&(A.moving&&A.moving.left===P.name?N:R)||'',background:'linear-gradient(to top, #eeeeee, #ffffff)',opacity:U?.8:1}),I=()=>({width:'100%',boxSizing:'border-box',padding:'0 8px',border:0,borderBottom:R,marginBottom:8}),M=({direction:A})=>w()('div',{style:{['asc'===A?'borderBottom':'borderTop']:'4px solid',borderLeft:'4px solid transparent',borderRight:'4px solid transparent'}});h.a=_.i(S.b)({onSortClink:(A)=>A.callback(_.i(C.b)(A.column.name)),onFilterInput:(A,P)=>A.callback(_.i(C.c)(A.column.name,P.target.value))})((A)=>w()('div',{style:O(A.state,A.column,A.last,A.ghost)},w()('div',{style:{display:'flex',alignItems:'center',height:30},onClick:A.onSortClink(A)},A.column.displayName||A.column.name,A.column.sort&&w()(M,{direction:A.column.sort})),A.column.enableFiltering&&w()('input',{style:I(),placeholder:A.column.placeholder,value:A.column.filter,onInput:A.onFilterInput(A)})))},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(3),C=_(5);const R='1px solid #d4d4d4',N=(M,A)=>({display:'flex',height:'inherit',cursor:'default',background:M.selectedIndex===A?'#c9dde1':0==A%2?'#fff':'#f3f3f3'}),O=(M,A)=>({flexShrink:0,display:'flex',alignItems:'center',width:M.width,padding:'0 8px',boxSizing:'border-box',borderLeft:R,borderRight:A?R:''}),I=_.i(S.b)({selectRow:(M)=>M.callback(_.i(C.a)(M.index))})((M)=>w()('div',{style:N(M.state,M.index),onClick:M.selectRow(M)},M.columns.map((A,P)=>w()('div',{style:O(A,P===M.columns.length-1)},w()('div',{style:{overflow:'hidden',whiteSpace:'nowrap',textOverflow:'ellipsis'}},M.datum[A.name])))));h.a=I},function(r,h,_){'use strict';var E=_(0),w=_.n(E);h.a=(S)=>(C)=>(R)=>w()(C,Object.assign({},R,S(R)))},function(r,h){'use strict';h.a=(E,w)=>{if(0>w||0===E.length)return[-1,w];let S=0;return E.reduce((C,{width:R},N)=>{const O=S;return S+=R,O<=w&&ww()('div',{className:'App__viewport'},w()('div',{className:'App__container'},w()('div',{className:'App__nav'},S.a.map((O)=>w()('a',{key:O.href,className:'App__link',href:`#${O.href}`},O.title))),w()('div',{className:'App__main'},N.children)))},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(25),C=_(71),R=_.n(C);h.a=(N)=>w()('div',{className:'Page__container'},w()('div',{className:'Page__header'},N.title),w()('div',{className:'Page__content'},w()('div',{className:'Page__demo'},w()(N.component,null)),w()('div',{className:'Page__code'},w()(S.a,{value:N.code}))))},function(r,h,_){'use strict';function E(Y){return Y&&Y.__esModule?Y:{default:Y}}h.__esModule=!0;var w=Object.assign||function(Y){for(var z=1,V;z({tableWidth:P.reduce((T,U)=>T+U.width,0)})),_.i(I.e)(['columnState','callback','columnComponent'],({columnState:P,callback:T,columnComponent:U})=>({columnComponent:(D)=>w()(U,Object.assign({state:P,callback:T},D))})),_.i(I.e)(['rowState','columns','callback','rowComponent'],({rowState:P,columns:T,callback:U,rowComponent:D})=>({rowComponent:(B)=>w()(D,Object.assign({state:P,columns:T,callback:U},B))})))(class extends C.a{constructor(T){super(T),this.state={ghost:!1,ghostX:0},this.onResizing=this.onResizing.bind(this),this.onResize=this.onResize.bind(this),this.onMoving=this.onMoving.bind(this),this.onMove=this.onMove.bind(this)}onResizing(T,U){this.setState({ghost:!0,ghostX:U})}onResize(T,U){this.setState({ghost:!1}),this.props.callback(_.i(A.d)(T,U))}onMoving(T,U,D){(T!==this.movingName||U!==this.movingLeft||D!==this.movingRight)&&(this.movingName=T,this.movingLeft=U,this.movingRight=D,this.props.callback(_.i(A.e)(T,U,D)))}onMove(T,U,D){this.movingName=null,this.movingLeft=null,this.movingRight=null,this.props.callback(_.i(A.f)(T,U,D))}render(T,{ghost:U,ghostX:D}){return w()('div',{style:{position:'relative',width:T.tableWidth,flexShrink:0}},!!T.headerHeight&&w()('div',{style:{position:'sticky',zIndex:1,top:0,height:T.headerHeight}},w()(R.a,{columns:T.columns,component:T.columnComponent,onMove:this.onMove,onMoving:this.onMoving,onResize:this.onResize,onResizing:this.onResizing})),U&&w()(N.a,{x:D}),w()(O.a,{data:T.data,scrollTop:T.scrollTop-T.headerHeight,viewportHeight:T.viewportHeight-T.headerHeight,rowHeight:T.rowHeight,component:T.rowComponent}))}})},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(1),C=_.n(S),R=_(2),N=_(3);const O=3,I=({children:D})=>w()('div',{style:{display:'flex',position:'relative',height:'inherit'}},D),M=({last:D})=>w()('div',{style:{position:'absolute',zIndex:2,top:0,bottom:0,right:D?0:-O,width:D?O:2*O,cursor:'col-resize'}}),A=({column:D,index:B,last:W,component:H})=>w()('div',{style:{position:'relative',width:D.width,height:'inherit'}},w()(H,{column:D,index:B,last:W,ghost:!1}),D.enableResizing&&w()(M,{last:W})),P=({x:D,column:B,index:W,component:H})=>w()('div',{style:{position:'absolute',transform:`translateX(${D}px)`,width:B.width,height:'inherit'}},w()(H,{column:B,index:W,last:!1,ghost:!0})),T=({columns:D,x:B,dx:W,onResizing:H,onMoving:Y})=>{const[z,V]=_.i(R.f)(D,B-W),K=D[z];if(_Mathabs(V)<=O){if(0!==z){const G=D[z-1];G.enableResizing&&H(G.name,B)}}else if(_Mathabs(V-K.width)<=O)K.enableResizing&&H(K.name,B);else if(K.enableMoving){const[G,$]=_.i(R.g)(D,B-V);return Y(K.name,-1===G?null:D[G].name,-1===$?null:D[$].name),{moving:!0,movingPosition:B-V,movingColumn:K,movingIndex:z}}},U=({columns:D,x:B,dx:W,onResize:H,onMove:Y})=>{const[z,V]=_.i(R.f)(D,B-W),K=D[z];if(_Mathabs(V)<=O){if(0!==z){const G=D[z-1];G.enableResizing&&H(G.name,_.i(R.h)(G,G.width+V+W))}}else if(_Mathabs(V-K.width)<=O)K.enableResizing&&H(K.name,_.i(R.h)(K,V+W));else if(K.enableMoving){const[G,$]=_.i(R.g)(D,B-V);return Y(K.name,-1===G?null:D[G].name,-1===$?null:D[$].name),{moving:!1}}};h.a=_.i(N.h)({offset:O,style:{height:'inherit'}})(class extends C.a{componentWillReceiveProps(B){this.props.dragging&&this.props.x!==B.x&&this.setState(T(B)),B.dragging||this.props.dragging===B.dragging||this.setState(U(B))}render({columns:B,component:W},{moving:H,movingColumn:Y,movingIndex:z,movingPosition:V}){return w()(I,null,B.map((K,G)=>w()(A,{key:K.name,column:K,index:G,last:G===B.length-1,component:W})),H&&w()(P,{x:V,column:Y,index:z,component:W}))}})},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(3),C=_(2);const R=({height:O,renderedTop:I,children:M})=>w()('div',{style:{position:'relative',height:O}},w()('div',{style:{position:'absolute',left:0,right:0,top:I}},M)),N=_.i(C.b)(_.i(S.f)(_.i(C.c)('height','component','datum')))(({height:O,datum:I,index:M,component:A})=>w()('div',{style:{height:O}},w()(A,{datum:I,index:M})));h.a=_.i(C.b)(_.i(S.g)((O,I={})=>{const[M,A]=_.i(C.d)({scrollTop:O.scrollTop,viewportHeight:O.viewportHeight,rowHeight:O.rowHeight,rowsCount:O.data.length});return{start:M,end:A,keys:_.i(C.e)(I.keys,M,A)}}),_.i(S.f)(_.i(C.c)('start','end','data','rowHeight','component')))(({data:O,rowHeight:I,component:M,start:A,end:P,keys:T})=>w()(R,{height:O.length*I,renderedTop:A*I},O.slice(A,P+1).map((U,D)=>w()(N,{key:T[A+D],height:I,index:A+D,datum:U,component:M}))))},function(r,h,_){'use strict';var E=_(0),w=_.n(E);h.a=({x:S})=>w()('div',{style:{position:'absolute',zIndex:3,top:0,bottom:0,borderLeft:'1px dotted #000',pointerEvents:'none',transform:`translateX(${S}px)`}})},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(1),C=_.n(S),R=_(53),N=_.n(R),O=_(54),I=_.n(O),M=_(69),A=_.n(M);N.a.registerLanguage('javascript',I.a);class P extends C.a{constructor(T){super(T),this.html=N.a.highlightAuto(T.value).value}componentWillReceiveProps(T){this.props.value!==T.value&&(this.html=N.a.highlightAuto(T.value).value)}render(){return w()('pre',{style:{margin:'0 0 0 15px'}},w()('code',{'class':'hljs',dangerouslySetInnerHTML:{__html:this.html}}))}}h.a=P},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(1),C=_.n(S),R=_(7);const N=_.i(R.a)(R.b),O=Array(1e5).fill(0).map((M,A)=>({col11:`Pinned left ${A}`,col1:A,col2:`Title ${A}`,col21:`Pinned right ${A}`,col3:'Lorem Ipsum is simply dummy text of the printing and typesetting industry.'}));class I extends C.a{constructor(){super(),this.state={gridState:{columns:[{name:'col1',enableSorting:!0},{name:'col11',width:120,pinnedLeft:!0,enableResizing:!0},{name:'col2',minWidth:60,enableMoving:!0,enableFiltering:!0,placeholder:'Search',width:150,enableResizing:!0},{name:'col21',width:120,pinnedRight:!0,enableResizing:!0},{name:'col3',displayName:'Column 3',width:200,maxWidth:300,enableMoving:!0,enableResizing:!0},{name:'4',width:'50%'}],rowState:{selectedIndex:0}},data:O,originalData:O},this.callback=this.callback.bind(this)}callback(M){switch(console.log(M),M.type){case'FILTER_COLUMN':case'SORT_COLUMN':{const A=_.i(R.c)(this.state.gridState,M);this.setState({gridState:A,data:_.i(R.d)(A,this.state.originalData)});break}default:this.setState({gridState:_.i(R.c)(this.state.gridState,M)});}}render({},{gridState:M,data:A}){return w()(N,{viewportWidth:600,viewportHeight:360,headerHeight:60,columnComponent:void 0,rowComponent:void 0,state:M,data:A,callback:this.callback})}}h.a=I},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(1),C=_.n(S),R=_(7);const N=_.i(R.a)(R.b),O={columns:[{name:'col1',width:'20%'},{name:'col2',width:'35%'},{name:'col3',width:'45%'}]},I=Array(1e5).fill(0).map((A,P)=>({col1:P,col2:`Col2 ${P}`,col3:'Lorem Ipsum is simply dummy text of the printing and typesetting industry.'}));class M extends C.a{render(){return w()(N,{viewportWidth:720,viewportHeight:480,state:O,data:I})}}h.a=M},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(7);const C=_.i(S.a)(S.b),R={columns:[{name:'col1',width:'50%'},{name:'col11',width:120,pinnedLeft:!0},{name:'col2',width:'50%'},{name:'col21',width:120,pinnedRight:!0}]},N=Array(1e5).fill(0).map((O,I)=>({col11:`Pinned left ${I}`,col1:I,col2:`Title ${I}`,col21:`Pinned right ${I}`,col3:'Lorem Ipsum is simply dummy text of the printing and typesetting industry.'}));h.a=()=>w()(C,{viewportWidth:600,viewportHeight:360,state:R,data:N})},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(1),C=_.n(S);h.a=({offset:R=3,style:N={}}={})=>(O)=>class extends C.a{constructor(I){super(I),this.state={dragging:!1},this.onMouseDown=this.onMouseDown.bind(this)}onMouseDown(I){const M=I.currentTarget.getBoundingClientRect().left,A=I.clientX-M,P=(U)=>{U.preventDefault();const D=U.clientX-M,B=D-A;(this.state.dragging||_Mathabs(B)>R)&&this.setState({dragging:!0,dx:B,x:D})},T=(U)=>{document.removeEventListener('mousemove',P),document.removeEventListener('mouseup',T),(this.state.dragging||_Mathabs(U.clientX-M-A)>R)&&this.setState({dragging:!1})};document.addEventListener('mousemove',P),document.addEventListener('mouseup',T)}render(I,M){return w()('div',{onMouseDown:this.onMouseDown,style:N},w()(O,Object.assign({},I,M)))}}},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(1),C=_.n(S);h.a=(R)=>(N)=>class extends C.a{shouldComponentUpdate(O){return R(this.props,O)}render(O){return w()(N,O)}}},function(r,h,_){'use strict';var E=_(2),w=_(13),S=_(6),C=_(11),R=_(12);const N={},O=60,I=(M,A)=>{return'string'==typeof M&&'%'===M[M.length-1]?A*+M.slice(0,-1)/100:M};h.a=()=>_.i(E.b)(_.i(w.a)((M)=>({headerHeight:M.headerHeight||0,rowHeight:M.rowHeight||30,columns:M.state.columns,columnState:M.state.columnState||N,rowState:M.state.rowState||N})),_.i(w.a)((M)=>({columnComponent:M.columnComponent||C.a,rowComponent:M.rowComponent||R.a})),_.i(S.a)(_.i(E.c)('columns','viewportWidth'),(M)=>({columns:M.columns.map((A)=>Object.assign({},A,{width:I(A.width,M.viewportWidth)}))})),_.i(S.a)(_.i(E.c)('columns'),(M)=>({columns:M.columns.map((A)=>Object.assign({},A,{minWidth:A.minWidth||O,width:_Mathmax(A.width||0,A.minWidth||O)}))})))},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(1),C=_.n(S)},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(1),C=_.n(S);h.a=(R)=>(N)=>class extends C.a{constructor(O){super(O);const I=Object.keys(R),M={},A=I.reduce((P,T)=>{return P[T]=(...U)=>R[T](M[T],...U),P},{});this.handlers=I.reduce((P,T)=>{return P[T]=(U)=>{return M[T]=U,A[T]},P},{})}render(O){return w()(N,Object.assign({},O,this.handlers))}}},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(1),C=_.n(S);h.a=(R)=>(N)=>class extends C.a{constructor(O){super(O),this.state=R(O)}componentWillReceiveProps(O){this.setState(R(O,this.state))}render(O,I){return w()(N,Object.assign({},O,I))}}},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(2),C=_(6);h.a=()=>(R)=>_.i(S.b)(_.i(C.a)(_.i(S.c)('columns'),(N)=>({leftPinnedColumns:N.columns.filter((O)=>O.pinnedLeft),centerColumns:N.columns.filter((O)=>!O.pinnedLeft&&!O.pinnedRight),rightPinnedColumns:N.columns.filter((O)=>O.pinnedRight)})))((N)=>w()('div',{style:{display:'flex'}},0!==N.leftPinnedColumns.length&&w()('div',{style:{position:'sticky',zIndex:2,left:0}},w()(R,Object.assign({},N,{columns:N.leftPinnedColumns}))),w()(R,Object.assign({},N,{columns:N.centerColumns})),0!==N.rightPinnedColumns.length&&w()('div',{style:{position:'sticky',zIndex:2,right:0}},w()(R,Object.assign({},N,{columns:N.rightPinnedColumns})))))},function(r,h,_){'use strict';var E=_(0),w=_.n(E),S=_(1),C=_.n(S);h.a=(R)=>class extends C.a{constructor(N){super(N),this.state={scrollTop:0,scrollLeft:0,viewportWidth:N.viewportWidth,viewportHeight:N.viewportHeight},this.ref=(O)=>{this.element=O,O&&this.setState({viewportWidth:O.clientWidth,viewportHeight:O.clientHeight})},this.onScroll=(O)=>this.setState({scrollTop:O.target.scrollTop,scrollLeft:O.target.scrollLeft})}componentWillReceiveProps(N){(this.props.viewportWidth!==N.viewportWidth||this.props.viewportHeight!==N.viewportHeight)&&this.element&&this.setState({viewportWidth:this.element.clientWidth,viewportHeight:this.element.clientHeight})}render(N,O){return w()('div',{style:{width:N.viewportWidth,height:N.viewportHeight,overflow:'auto'},onScroll:this.onScroll,ref:this.ref},w()(R,Object.assign({},N,O)))}}},function(r,h){'use strict';h.a=(E={},w)=>{switch(w.type){case'MARK_MOVE_DEST':return Object.assign({},E,{moving:{name:w.name,left:w.left,right:w.right}});case'MOVE_COLUMN':return Object.assign({},E,{moving:null});case'RESIZE_COLUMN':return Object.assign({},E,{resizing:null});default:return E;}}},function(r,h){'use strict';h.a=(E=[],w)=>{switch(w.type){case'MOVE_COLUMN':if(w.left||w.right){const S=w.left?E.findIndex((C)=>C.name===w.left)+1:E.findIndex((C)=>C.name===w.right);return[...E.slice(0,S).filter((C)=>C.name!==w.name),...E.filter((C)=>C.name===w.name),...E.slice(S).filter((C)=>C.name!==w.name)]}return E;case'RESIZE_COLUMN':return E.map((S)=>{return S.name===w.name?Object.assign({},S,{width:w.size}):S});case'FILTER_COLUMN':return E.map((S)=>{return S.name===w.name?Object.assign({},S,{filter:w.filter}):S});case'SORT_COLUMN':return E.map((S)=>{return S.name===w.name?Object.assign({},S,{sort:!S.sort&&'asc'||'asc'===S.sort&&'desc'||'desc'===S.sort&&null}):S.sort?Object.assign({},S,{sort:null}):S});default:return E;}}},function(r,h,_){'use strict';var E=_(38),w=_(37),S=_(40);h.a=(C={},R)=>Object.assign({},C,{columns:_.i(E.a)(C.columns,R),columnState:_.i(w.a)(C.columnState,R),rowState:_.i(S.a)(C.rowState,R)})},function(r,h){'use strict';h.a=(E={},w)=>{switch(w.type){case'SELECT_ROW':return Object.assign({},E,{selectedIndex:w.rowIndex});default:return E;}}},function(r,h,_){'use strict';var E=_(14);h.a=(w,S)=>{if(2>w.length)return[-1,0];const[C,R]=_.i(E.a)(w,S);if(-1===C)return[-1,0];if(C===w.length)return[w.length-1,-1];const N=w[C].width;return R(w,S)=>E.some((C)=>w[C]!==S[C])},function(r,h){'use strict';h.a=function(...w){return 0===w.length?(S)=>S:1===w.length?w[0]:w.reduce((S,C)=>(...R)=>S(C(...R)))}},function(r,h){'use strict';const E=()=>Object.create(null),w=(N)=>Object.keys(N).reduce((O,I)=>(O[N[I]]=I,O),E()),S=(N)=>{let O=0;return()=>{for(;;){const I=`key_${O}`;if(O+=1,!(I in N))return I}}},C=(N,O,I)=>{const M=E();for(let A=O;A{const M=E(),A=S(w(N));for(let P=O;P{const M=C(N,O,I),A=R(M,O,I);return Object.assign(E(),M,A)}},function(r,h){'use strict';h.a=({scrollTop:E,viewportHeight:w,rowHeight:S,rowsCount:C})=>{if(0>w)return[0,0];const R=Math.ceil(w/S),N=_Mathfloor(_Mathfloor(_Mathmax(0,E)/S)/R),O=_Mathmin(C,(N+2)*R),I=_Mathmax(0,O-2*R);return[I,O]}},function(r,h){'use strict';const E=(S,C)=>C.every((R)=>-1!==S[R.name].toLowerCase().indexOf(R.filter)),w=(S,C,R)=>{return S[R.name]===C[R.name]?0:'asc'===R.sort?S[R.name]C[R.name]?-1:1};h.a=(S,C)=>{const R=S.columns.filter((M)=>M.filter),N=S.columns.find((M)=>'asc'===M.sort||'desc'===M.sort),O=R.length?C.filter((M)=>E(M,R)):C,I=N?O.slice().sort((M,A)=>w(M,A,N)):O;return I}},function(r,h){'use strict';h.a=(E,w)=>_Mathmin(_Mathmax(E.minWidth||0,w),E.maxWidth||Infinity)},function(r,h){'use strict';function E(U){var D=U.length;if(0>18]+O[63&U>>12]+O[63&U>>6]+O[63&U]}function R(U,D,B){for(var H=[],Y=D,W;Y>16,z[K++]=255&H>>8,z[K++]=255&H;return 2===Y?(H=I[U.charCodeAt(D)]<<2|I[U.charCodeAt(D+1)]>>4,z[K++]=255&H):1===Y&&(H=I[U.charCodeAt(D)]<<10|I[U.charCodeAt(D+1)]<<4|I[U.charCodeAt(D+2)]>>2,z[K++]=255&H>>8,z[K++]=255&H),z},h.fromByteArray=function(U){for(var B=U.length,W=B%3,H='',Y=[],z=16383,V=0,K=B-W,D;VK?K:V+z));return 1==W?(D=U[B-1],H+=O[D>>2],H+=O[63&D<<4],H+='=='):2==W&&(D=(U[B-2]<<8)+U[B-1],H+=O[D>>10],H+=O[63&D>>4],H+=O[63&D<<2],H+='='),Y.push(H),Y.join('')};for(var O=[],I=[],M='undefined'==typeof Uint8Array?Array:Uint8Array,A='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',P=0,T=A.length;PCe)throw new RangeError('"size" argument must not be negative')}function I(Ce,Re,Ne,Oe){return O(Re),0>=Re?C(Ce,Re):void 0===Ne?C(Ce,Re):'string'==typeof Oe?C(Ce,Re).fill(Ne,Oe):C(Ce,Re).fill(Ne)}function M(Ce,Re){if(O(Re),Ce=C(Ce,0>Re?0:0|D(Re)),!R.TYPED_ARRAY_SUPPORT)for(var Ne=0;NeRe.length?0:0|D(Re.length);Ce=C(Ce,Ne);for(var Oe=0;OeNe||Re.byteLength=S())throw new RangeError('Attempt to allocate Buffer larger than maximum size: 0x'+S().toString(16)+' bytes');return 0|Ce}function W(Ce,Re){if(R.isBuffer(Ce))return Ce.length;if('undefined'!=typeof ArrayBuffer&&'function'==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(Ce)||Ce instanceof ArrayBuffer))return Ce.byteLength;'string'!=typeof Ce&&(Ce=''+Ce);var Ne=Ce.length;if(0===Ne)return 0;for(var Oe=!1;;)switch(Re){case'ascii':case'latin1':case'binary':return Ne;case'utf8':case'utf-8':case void 0:return me(Ce).length;case'ucs2':case'ucs-2':case'utf16le':case'utf-16le':return 2*Ne;case'hex':return Ne>>>1;case'base64':return be(Ce).length;default:if(Oe)return me(Ce).length;Re=(''+Re).toLowerCase(),Oe=!0;}}function H(Ce,Re,Ne){var Oe=!1;if((void 0===Re||0>Re)&&(Re=0),Re>this.length)return'';if((void 0===Ne||Ne>this.length)&&(Ne=this.length),0>=Ne)return'';if(Ne>>>=0,Re>>>=0,Ne<=Re)return'';for(Ce||(Ce='utf8');;)switch(Ce){case'hex':return ne(this,Re,Ne);case'utf8':case'utf-8':return Z(this,Re,Ne);case'ascii':return ee(this,Re,Ne);case'latin1':case'binary':return te(this,Re,Ne);case'base64':return J(this,Re,Ne);case'ucs2':case'ucs-2':case'utf16le':case'utf-16le':return oe(this,Re,Ne);default:if(Oe)throw new TypeError('Unknown encoding: '+Ce);Ce=(Ce+'').toLowerCase(),Oe=!0;}}function Y(Ce,Re,Ne){var Oe=Ce[Re];Ce[Re]=Ce[Ne],Ce[Ne]=Oe}function z(Ce,Re,Ne,Oe,Ie){if(0===Ce.length)return-1;if('string'==typeof Ne?(Oe=Ne,Ne=0):2147483647Ne&&(Ne=-2147483648),Ne=+Ne,isNaN(Ne)&&(Ne=Ie?0:Ce.length-1),0>Ne&&(Ne=Ce.length+Ne),Ne>=Ce.length){if(Ie)return-1;Ne=Ce.length-1}else if(0>Ne)if(Ie)Ne=0;else return-1;if('string'==typeof Re&&(Re=R.from(Re,Oe)),R.isBuffer(Re))return 0===Re.length?-1:V(Ce,Re,Ne,Oe,Ie);if('number'==typeof Re)return Re&=255,R.TYPED_ARRAY_SUPPORT&&'function'==typeof Uint8Array.prototype.indexOf?Ie?Uint8Array.prototype.indexOf.call(Ce,Re,Ne):Uint8Array.prototype.lastIndexOf.call(Ce,Re,Ne):V(Ce,[Re],Ne,Oe,Ie);throw new TypeError('val must be string, number or Buffer')}function V(Ce,Re,Ne,Oe,Ie){function Me(je,We){return 1==Ae?je[We]:je.readUInt16BE(We*Ae)}var Ae=1,Pe=Ce.length,Te=Re.length;if(void 0!==Oe&&(Oe=(Oe+'').toLowerCase(),'ucs2'===Oe||'ucs-2'===Oe||'utf16le'===Oe||'utf-16le'===Oe)){if(2>Ce.length||2>Re.length)return-1;Ae=2,Pe/=2,Te/=2,Ne/=2}var Ue;if(Ie){var Le=-1;for(Ue=Ne;UePe&&(Ne=Pe-Te),Ue=Ne;0<=Ue;Ue--){for(var De=!0,Be=0;BeIe&&(Oe=Ie)):Oe=Ie;var Me=Re.length;if(0!=Me%2)throw new TypeError('Invalid hex string');Oe>Me/2&&(Oe=Me/2);for(var Ae=0,Pe;AeMe&&(Ae=Me):2==Pe?(Te=Ce[Ie+1],128==(192&Te)&&(De=(31&Me)<<6|63&Te,127De||57343De&&(Ae=De))):void 0}null===Ae?(Ae=65533,Pe=1):65535>>10),Ae=56320|1023&Ae),Oe.push(Ae),Ie+=Pe}return Q(Oe)}function Q(Ce){var Re=Ce.length;if(Re<=Se)return _StringfromCharCode.apply(String,Ce);for(var Ne='',Oe=0;OeRe)&&(Re=0),(!Ne||0>Ne||Ne>Oe)&&(Ne=Oe);for(var Ie='',Me=Re;MeCe)throw new RangeError('offset is not uint');if(Ce+Re>Ne)throw new RangeError('Trying to access beyond buffer length')}function ie(Ce,Re,Ne,Oe,Ie,Me){if(!R.isBuffer(Ce))throw new TypeError('"buffer" argument must be a Buffer instance');if(Re>Ie||ReCe.length)throw new RangeError('Index out of range')}function se(Ce,Re,Ne,Oe){0>Re&&(Re=65535+Re+1);for(var Ie=0,Me=_Mathmin(Ce.length-Ne,2);Ie>>8*(Oe?Ie:1-Ie)}function le(Ce,Re,Ne,Oe){0>Re&&(Re=4294967295+Re+1);for(var Ie=0,Me=_Mathmin(Ce.length-Ne,4);Ie>>8*(Oe?Ie:3-Ie)}function de(Ce,Re,Ne,Oe){if(Ne+Oe>Ce.length)throw new RangeError('Index out of range');if(0>Ne)throw new RangeError('Index out of range')}function pe(Ce,Re,Ne,Oe,Ie){return Ie||de(Ce,Re,Ne,4,3.4028234663852886e38,-3.4028234663852886e38),Ee.write(Ce,Re,Ne,Oe,23,4),Ne+4}function ue(Ce,Re,Ne,Oe,Ie){return Ie||de(Ce,Re,Ne,8,1.7976931348623157e308,-1.7976931348623157e308),Ee.write(Ce,Re,Ne,Oe,52,8),Ne+8}function ce(Ce){if(Ce=fe(Ce).replace(ke,''),2>Ce.length)return'';for(;0!=Ce.length%4;)Ce+='=';return Ce}function fe(Ce){return Ce.trim?Ce.trim():Ce.replace(/^\s+|\s+$/g,'')}function he(Ce){return 16>Ce?'0'+Ce.toString(16):Ce.toString(16)}function me(Ce,Re){Re=Re||Infinity;for(var Oe=Ce.length,Ie=null,Me=[],Ae=0,Ne;AeNe){if(!Ie){if(56319Ne){-1<(Re-=3)&&Me.push(239,191,189),Ie=Ne;continue}Ne=(Ie-55296<<10|Ne-56320)+65536}else Ie&&-1<(Re-=3)&&Me.push(239,191,189);if(Ie=null,128>Ne){if(0>(Re-=1))break;Me.push(Ne)}else if(2048>Ne){if(0>(Re-=2))break;Me.push(192|Ne>>6,128|63&Ne)}else if(65536>Ne){if(0>(Re-=3))break;Me.push(224|Ne>>12,128|63&Ne>>6,128|63&Ne)}else if(1114112>Ne){if(0>(Re-=4))break;Me.push(240|Ne>>18,128|63&Ne>>12,128|63&Ne>>6,128|63&Ne)}else throw new Error('Invalid code point')}return Me}function ge(Ce){for(var Re=[],Ne=0;Ne(Re-=2));++Ae)Ne=Ce.charCodeAt(Ae),Oe=Ne>>8,Ie=Ne%256,Me.push(Ie),Me.push(Oe);return Me}function be(Ce){return xe.toByteArray(ce(Ce))}function ve(Ce,Re,Ne,Oe){for(var Ie=0;Ie=Re.length||Ie>=Ce.length);++Ie)Re[Ie+Ne]=Ce[Ie];return Ie}function _e(Ce){return Ce!==Ce}/*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */var xe=_(48),Ee=_(58),we=_(64);h.Buffer=R,h.SlowBuffer=function(Ce){return+Ce!=Ce&&(Ce=0),R.alloc(+Ce)},h.INSPECT_MAX_BYTES=50,R.TYPED_ARRAY_SUPPORT=E.TYPED_ARRAY_SUPPORT===void 0?function(){try{var Ce=new Uint8Array(1);return Ce.__proto__={__proto__:Uint8Array.prototype,foo:function(){return 42}},42===Ce.foo()&&'function'==typeof Ce.subarray&&0===Ce.subarray(1,1).byteLength}catch(Re){return!1}}():E.TYPED_ARRAY_SUPPORT,h.kMaxLength=S(),R.poolSize=8192,R._augment=function(Ce){return Ce.__proto__=R.prototype,Ce},R.from=function(Ce,Re,Ne){return N(null,Ce,Re,Ne)},R.TYPED_ARRAY_SUPPORT&&(R.prototype.__proto__=Uint8Array.prototype,R.__proto__=Uint8Array,'undefined'!=typeof Symbol&&Symbol.species&&R[Symbol.species]===R&&Object.defineProperty(R,Symbol.species,{value:null,configurable:!0})),R.alloc=function(Ce,Re,Ne){return I(null,Ce,Re,Ne)},R.allocUnsafe=function(Ce){return M(null,Ce)},R.allocUnsafeSlow=function(Ce){return M(null,Ce)},R.isBuffer=function(Re){return!!(null!=Re&&Re._isBuffer)},R.compare=function(Re,Ne){if(!R.isBuffer(Re)||!R.isBuffer(Ne))throw new TypeError('Arguments must be Buffers');if(Re===Ne)return 0;for(var Oe=Re.length,Ie=Ne.length,Me=0,Ae=_Mathmin(Oe,Ie);MeNe&&(Re+=' ... ')),''},R.prototype.compare=function(Re,Ne,Oe,Ie,Me){if(!R.isBuffer(Re))throw new TypeError('Argument must be a Buffer');if(void 0===Ne&&(Ne=0),void 0===Oe&&(Oe=Re?Re.length:0),void 0===Ie&&(Ie=0),void 0===Me&&(Me=this.length),0>Ne||Oe>Re.length||0>Ie||Me>this.length)throw new RangeError('out of range index');if(Ie>=Me&&Ne>=Oe)return 0;if(Ie>=Me)return-1;if(Ne>=Oe)return 1;if(Ne>>>=0,Oe>>>=0,Ie>>>=0,Me>>>=0,this===Re)return 0;for(var Ae=Me-Ie,Pe=Oe-Ne,Te=_Mathmin(Ae,Pe),Ue=this.slice(Ie,Me),Le=Re.slice(Ne,Oe),De=0;DeMe)&&(Oe=Me),0Oe||0>Ne)||Ne>this.length)throw new RangeError('Attempt to write outside buffer bounds');Ie||(Ie='utf8');for(var Ae=!1;;)switch(Ie){case'hex':return K(this,Re,Ne,Oe);case'utf8':case'utf-8':return G(this,Re,Ne,Oe);case'ascii':return $(this,Re,Ne,Oe);case'latin1':case'binary':return F(this,Re,Ne,Oe);case'base64':return X(this,Re,Ne,Oe);case'ucs2':case'ucs-2':case'utf16le':case'utf-16le':return q(this,Re,Ne,Oe);default:if(Ae)throw new TypeError('Unknown encoding: '+Ie);Ie=(''+Ie).toLowerCase(),Ae=!0;}},R.prototype.toJSON=function(){return{type:'Buffer',data:Array.prototype.slice.call(this._arr||this,0)}};var Se=4096;R.prototype.slice=function(Re,Ne){var Oe=this.length;Re=~~Re,Ne=Ne===void 0?Oe:~~Ne,0>Re?(Re+=Oe,0>Re&&(Re=0)):Re>Oe&&(Re=Oe),0>Ne?(Ne+=Oe,0>Ne&&(Ne=0)):Ne>Oe&&(Ne=Oe),Ne=Me&&(Ie-=_Mathpow(2,8*Ne)),Ie},R.prototype.readIntBE=function(Re,Ne,Oe){Re|=0,Ne|=0,Oe||ae(Re,Ne,this.length);for(var Ie=Ne,Me=1,Ae=this[Re+--Ie];0=Me&&(Ae-=_Mathpow(2,8*Ne)),Ae},R.prototype.readInt8=function(Re,Ne){return Ne||ae(Re,1,this.length),128&this[Re]?-1*(255-this[Re]+1):this[Re]},R.prototype.readInt16LE=function(Re,Ne){Ne||ae(Re,2,this.length);var Oe=this[Re]|this[Re+1]<<8;return 32768&Oe?4294901760|Oe:Oe},R.prototype.readInt16BE=function(Re,Ne){Ne||ae(Re,2,this.length);var Oe=this[Re+1]|this[Re]<<8;return 32768&Oe?4294901760|Oe:Oe},R.prototype.readInt32LE=function(Re,Ne){return Ne||ae(Re,4,this.length),this[Re]|this[Re+1]<<8|this[Re+2]<<16|this[Re+3]<<24},R.prototype.readInt32BE=function(Re,Ne){return Ne||ae(Re,4,this.length),this[Re]<<24|this[Re+1]<<16|this[Re+2]<<8|this[Re+3]},R.prototype.readFloatLE=function(Re,Ne){return Ne||ae(Re,4,this.length),Ee.read(this,Re,!0,23,4)},R.prototype.readFloatBE=function(Re,Ne){return Ne||ae(Re,4,this.length),Ee.read(this,Re,!1,23,4)},R.prototype.readDoubleLE=function(Re,Ne){return Ne||ae(Re,8,this.length),Ee.read(this,Re,!0,52,8)},R.prototype.readDoubleBE=function(Re,Ne){return Ne||ae(Re,8,this.length),Ee.read(this,Re,!1,52,8)},R.prototype.writeUIntLE=function(Re,Ne,Oe,Ie){if(Re=+Re,Ne|=0,Oe|=0,!Ie){var Me=_Mathpow(2,8*Oe)-1;ie(this,Re,Ne,Oe,Me,0)}var Ae=1,Pe=0;for(this[Ne]=255ℜ++Pe>>8):se(this,Re,Ne,!0),Ne+2},R.prototype.writeUInt16BE=function(Re,Ne,Oe){return Re=+Re,Ne|=0,Oe||ie(this,Re,Ne,2,65535,0),R.TYPED_ARRAY_SUPPORT?(this[Ne]=Re>>>8,this[Ne+1]=255&Re):se(this,Re,Ne,!1),Ne+2},R.prototype.writeUInt32LE=function(Re,Ne,Oe){return Re=+Re,Ne|=0,Oe||ie(this,Re,Ne,4,4294967295,0),R.TYPED_ARRAY_SUPPORT?(this[Ne+3]=Re>>>24,this[Ne+2]=Re>>>16,this[Ne+1]=Re>>>8,this[Ne]=255&Re):le(this,Re,Ne,!0),Ne+4},R.prototype.writeUInt32BE=function(Re,Ne,Oe){return Re=+Re,Ne|=0,Oe||ie(this,Re,Ne,4,4294967295,0),R.TYPED_ARRAY_SUPPORT?(this[Ne]=Re>>>24,this[Ne+1]=Re>>>16,this[Ne+2]=Re>>>8,this[Ne+3]=255&Re):le(this,Re,Ne,!1),Ne+4},R.prototype.writeIntLE=function(Re,Ne,Oe,Ie){if(Re=+Re,Ne|=0,!Ie){var Me=_Mathpow(2,8*Oe-1);ie(this,Re,Ne,Oe,Me-1,-Me)}var Ae=0,Pe=1,Te=0;for(this[Ne]=255ℜ++AeRe&&0==Te&&0!==this[Ne+Ae-1]&&(Te=1),this[Ne+Ae]=255&(Re/Pe>>0)-Te;return Ne+Oe},R.prototype.writeIntBE=function(Re,Ne,Oe,Ie){if(Re=+Re,Ne|=0,!Ie){var Me=_Mathpow(2,8*Oe-1);ie(this,Re,Ne,Oe,Me-1,-Me)}var Ae=Oe-1,Pe=1,Te=0;for(this[Ne+Ae]=255ℜ0<=--Ae&&(Pe*=256);)0>Re&&0==Te&&0!==this[Ne+Ae+1]&&(Te=1),this[Ne+Ae]=255&(Re/Pe>>0)-Te;return Ne+Oe},R.prototype.writeInt8=function(Re,Ne,Oe){return Re=+Re,Ne|=0,Oe||ie(this,Re,Ne,1,127,-128),R.TYPED_ARRAY_SUPPORT||(Re=_Mathfloor(Re)),0>Re&&(Re=255+Re+1),this[Ne]=255&Re,Ne+1},R.prototype.writeInt16LE=function(Re,Ne,Oe){return Re=+Re,Ne|=0,Oe||ie(this,Re,Ne,2,32767,-32768),R.TYPED_ARRAY_SUPPORT?(this[Ne]=255&Re,this[Ne+1]=Re>>>8):se(this,Re,Ne,!0),Ne+2},R.prototype.writeInt16BE=function(Re,Ne,Oe){return Re=+Re,Ne|=0,Oe||ie(this,Re,Ne,2,32767,-32768),R.TYPED_ARRAY_SUPPORT?(this[Ne]=Re>>>8,this[Ne+1]=255&Re):se(this,Re,Ne,!1),Ne+2},R.prototype.writeInt32LE=function(Re,Ne,Oe){return Re=+Re,Ne|=0,Oe||ie(this,Re,Ne,4,2147483647,-2147483648),R.TYPED_ARRAY_SUPPORT?(this[Ne]=255&Re,this[Ne+1]=Re>>>8,this[Ne+2]=Re>>>16,this[Ne+3]=Re>>>24):le(this,Re,Ne,!0),Ne+4},R.prototype.writeInt32BE=function(Re,Ne,Oe){return Re=+Re,Ne|=0,Oe||ie(this,Re,Ne,4,2147483647,-2147483648),0>Re&&(Re=4294967295+Re+1),R.TYPED_ARRAY_SUPPORT?(this[Ne]=Re>>>24,this[Ne+1]=Re>>>16,this[Ne+2]=Re>>>8,this[Ne+3]=255&Re):le(this,Re,Ne,!1),Ne+4},R.prototype.writeFloatLE=function(Re,Ne,Oe){return pe(this,Re,Ne,!0,Oe)},R.prototype.writeFloatBE=function(Re,Ne,Oe){return pe(this,Re,Ne,!1,Oe)},R.prototype.writeDoubleLE=function(Re,Ne,Oe){return ue(this,Re,Ne,!0,Oe)},R.prototype.writeDoubleBE=function(Re,Ne,Oe){return ue(this,Re,Ne,!1,Oe)},R.prototype.copy=function(Re,Ne,Oe,Ie){if(Oe||(Oe=0),Ie||0===Ie||(Ie=this.length),Ne>=Re.length&&(Ne=Re.length),Ne||(Ne=0),0Ne)throw new RangeError('targetStart out of bounds');if(0>Oe||Oe>=this.length)throw new RangeError('sourceStart out of bounds');if(0>Ie)throw new RangeError('sourceEnd out of bounds');Ie>this.length&&(Ie=this.length),Re.length-NeMe||!R.TYPED_ARRAY_SUPPORT)for(Ae=0;AeMe&&(Re=Me)}if(void 0!==Ie&&'string'!=typeof Ie)throw new TypeError('encoding must be a string');if('string'==typeof Ie&&!R.isEncoding(Ie))throw new TypeError('Unknown encoding: '+Ie)}else'number'==typeof Re&&(Re&=255);if(0>Ne||this.length>>=0,Oe=Oe===void 0?this.length:Oe>>>0,Re||(Re=0);var Ae;if('number'==typeof Re)for(Ae=Ne;Ae]/gm,function(ae){return ne[ae]})}function S(oe){return oe.nodeName.toLowerCase()}function C(oe,ae){var ie=oe&&oe.exec(ae);return ie&&0===ie.index}function R(oe){return J.test(oe)}function N(oe){var de=oe.className+' ',ae,ie,se,le;if(de+=oe.parentNode?oe.parentNode.className:'',ie=Z.exec(de),ie)return G(ie[1])?ie[1]:'no-highlight';for(de=de.split(/\s+/),ae=0,se=de.length;ae'}function de(me){ce+=''}function pe(me){('start'===me.event?le:de)(me.node)}for(var ue=0,ce='',fe=[],he;oe.length||ae.length;)if(he=se(),ce+=w(ie.substring(ue,he[0].offset)),ue=he[0].offset,he===oe){fe.reverse().forEach(de);do pe(he.splice(0,1)[0]),he=se();while(he===oe&&he.length&&he[0].offset===ue);fe.reverse().forEach(le)}else'start'===he[0].event?fe.push(he[0].node):fe.pop(),pe(he.splice(0,1)[0]);return ce+w(ie.substr(ue))}function A(oe){return oe.variants&&!oe.cached_variants&&(oe.cached_variants=oe.variants.map(function(ae){return O(oe,{variants:null},ae)})),oe.cached_variants||oe.endsWithParent&&[O(oe)]||[oe]}function P(oe){function ae(le){return le&&le.source||le}function ie(le,de){return new RegExp(ae(le),'m'+(oe.case_insensitive?'i':'')+(de?'g':''))}function se(le,de){if(!le.compiled){if(le.compiled=!0,le.keywords=le.keywords||le.beginKeywords,le.keywords){var pe={},ue=function(fe,he){oe.case_insensitive&&(he=he.toLowerCase()),he.split(' ').forEach(function(me){var ge=me.split('|');pe[ge[0]]=[fe,ge[1]?+ge[1]:1]})};'string'==typeof le.keywords?ue('keyword',le.keywords):F(le.keywords).forEach(function(fe){ue(fe,le.keywords[fe])}),le.keywords=pe}le.lexemesRe=ie(le.lexemes||/\w+/,!0),de&&(le.beginKeywords&&(le.begin='\\b('+le.beginKeywords.split(' ').join('|')+')\\b'),!le.begin&&(le.begin=/\B|\b/),le.beginRe=ie(le.begin),!le.end&&!le.endsWithParent&&(le.end=/\B|\b/),le.end&&(le.endRe=ie(le.end)),le.terminator_end=ae(le.end)||'',le.endsWithParent&&de.terminator_end&&(le.terminator_end+=(le.end?'|':'')+de.terminator_end)),le.illegal&&(le.illegalRe=ie(le.illegal)),null==le.relevance&&(le.relevance=1),le.contains||(le.contains=[]),le.contains=Array.prototype.concat.apply([],le.contains.map(function(fe){return A('self'===fe?le:fe)})),le.contains.forEach(function(fe){se(fe,le)}),le.starts&&se(le.starts,de);var ce=le.contains.map(function(fe){return fe.beginKeywords?'\\.?('+fe.begin+')\\.?':fe.begin}).concat([le.terminator_end,le.illegal]).map(ae).filter(Boolean);le.terminators=ce.length?ie(ce.join('|'),!0):{exec:function(){return null}}}}se(oe)}function T(oe,ae,ie,se){function le(Ne,Oe){var Ie,Me;for(Ie=0,Me=Oe.contains.length;Ie',Pe+Oe+Te}function fe(){var Ne,Oe,Ie,Me;if(!ve.keywords)return w(we);for(Me='',Oe=0,ve.lexemesRe.lastIndex=0,Ie=ve.lexemesRe.exec(we);Ie;)Me+=w(we.substring(Oe,Ie.index)),Ne=ue(ve,Ie),Ne?(Se+=Ne[1],Me+=ce(Ne[0],w(Ie[0]))):Me+=w(Ie[0]),Oe=ve.lexemesRe.lastIndex,Ie=ve.lexemesRe.exec(we);return Me+w(we.substr(Oe))}function he(){var Ne='string'==typeof ve.subLanguage;if(Ne&&!X[ve.subLanguage])return w(we);var Oe=Ne?T(ve.subLanguage,we,!0,_e[ve.subLanguage]):U(we,ve.subLanguage.length?ve.subLanguage:void 0);return 0')+'"');return we+=Oe,Oe.length||1}var be=G(oe);if(!be)throw new Error('Unknown language: "'+oe+'"');P(be);var ve=se||be,_e={},xe='',Ee;for(Ee=ve;Ee!==be;Ee=Ee.parent)Ee.className&&(xe=ce(Ee.className,'',!0)+xe);var we='',Se=0;try{for(var Re=0,ke,Ce;ve.terminators.lastIndex=Re,ke=ve.terminators.exec(ae),!!ke;)Ce=ye(ae.substring(Re,ke.index),ke[0]),Re=ke.index+Ce;for(ye(ae.substr(Re)),Ee=ve;Ee.parent;Ee=Ee.parent)Ee.className&&(xe+=ee);return{relevance:Se,value:xe,language:oe,top:ve}}catch(Ne){if(Ne.message&&-1!==Ne.message.indexOf('Illegal'))return{relevance:0,value:w(ae)};throw Ne}}function U(oe,ae){ae=ae||te.languages||F(X);var ie={relevance:0,value:w(oe)},se=ie;return ae.filter(G).forEach(function(le){var de=T(le,oe,!1);de.language=le,de.relevance>se.relevance&&(se=de),de.relevance>ie.relevance&&(se=ie,ie=de)}),se.language&&(ie.second_best=se),ie}function D(oe){return te.tabReplace||te.useBR?oe.replace(Q,function(ae,ie){return te.useBR&&'\n'===ae?'
':te.tabReplace?ie.replace(/\t/g,te.tabReplace):''}):oe}function B(oe,ae,ie){var se=ae?q[ae]:ie,le=[oe.trim()];return oe.match(/\bhljs\b/)||le.push('hljs'),-1===oe.indexOf(se)&&le.push(se),le.join(' ').trim()}function W(oe){var pe=N(oe),ae,ie,se,le,de;R(pe)||(te.useBR?(ae=document.createElementNS('http://www.w3.org/1999/xhtml','div'),ae.innerHTML=oe.innerHTML.replace(/\n/g,'').replace(//g,'\n')):ae=oe,de=ae.textContent,se=pe?T(pe,de,!0):U(de),ie=I(ae),ie.length&&(le=document.createElementNS('http://www.w3.org/1999/xhtml','div'),le.innerHTML=se.value,se.value=M(ie,I(le),de)),se.value=D(se.value),oe.innerHTML=se.value,oe.className=B(oe.className,pe,se.language),oe.result={language:se.language,re:se.relevance},se.second_best&&(oe.second_best={language:se.second_best.language,re:se.second_best.relevance}))}function Y(){if(!Y.called){Y.called=!0;var oe=document.querySelectorAll('pre code');$.forEach.call(oe,W)}}function G(oe){return oe=(oe||'').toLowerCase(),X[oe]||X[q[oe]]}var $=[],F=Object.keys,X={},q={},J=/^(no-?highlight|plain|text)$/i,Z=/\blang(?:uage)?-([\w-]+)\b/i,Q=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,ee='',te={classPrefix:'hljs-',tabReplace:null,useBR:!1,languages:void 0},ne={'&':'&','<':'<','>':'>'};return E.highlight=T,E.highlightAuto=U,E.fixMarkup=D,E.highlightBlock=W,E.configure=function(oe){te=O(te,oe)},E.initHighlighting=Y,E.initHighlightingOnLoad=function(){addEventListener('DOMContentLoaded',Y,!1),addEventListener('load',Y,!1)},E.registerLanguage=function(oe,ae){var ie=X[oe]=ae(E);ie.aliases&&ie.aliases.forEach(function(se){q[se]=oe})},E.listLanguages=function(){return F(X)},E.getLanguage=G,E.inherit=O,E.IDENT_RE='[a-zA-Z]\\w*',E.UNDERSCORE_IDENT_RE='[a-zA-Z_]\\w*',E.NUMBER_RE='\\b\\d+(\\.\\d+)?',E.C_NUMBER_RE='(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)',E.BINARY_NUMBER_RE='\\b(0b[01]+)',E.RE_STARTERS_RE='!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~',E.BACKSLASH_ESCAPE={begin:'\\\\[\\s\\S]',relevance:0},E.APOS_STRING_MODE={className:'string',begin:'\'',end:'\'',illegal:'\\n',contains:[E.BACKSLASH_ESCAPE]},E.QUOTE_STRING_MODE={className:'string',begin:'"',end:'"',illegal:'\\n',contains:[E.BACKSLASH_ESCAPE]},E.PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},E.COMMENT=function(oe,ae,ie){var se=E.inherit({className:'comment',begin:oe,end:ae,contains:[]},ie||{});return se.contains.push(E.PHRASAL_WORDS_MODE),se.contains.push({className:'doctag',begin:'(?:TODO|FIXME|NOTE|BUG|XXX):',relevance:0}),se},E.C_LINE_COMMENT_MODE=E.COMMENT('//','$'),E.C_BLOCK_COMMENT_MODE=E.COMMENT('/\\*','\\*/'),E.HASH_COMMENT_MODE=E.COMMENT('#','$'),E.NUMBER_MODE={className:'number',begin:E.NUMBER_RE,relevance:0},E.C_NUMBER_MODE={className:'number',begin:E.C_NUMBER_RE,relevance:0},E.BINARY_NUMBER_MODE={className:'number',begin:E.BINARY_NUMBER_RE,relevance:0},E.CSS_NUMBER_MODE={className:'number',begin:E.NUMBER_RE+'(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?',relevance:0},E.REGEXP_MODE={className:'regexp',begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[E.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0,contains:[E.BACKSLASH_ESCAPE]}]},E.TITLE_MODE={className:'title',begin:E.IDENT_RE,relevance:0},E.UNDERSCORE_TITLE_MODE={className:'title',begin:E.UNDERSCORE_IDENT_RE,relevance:0},E.METHOD_GUARD={begin:'\\.\\s*'+E.UNDERSCORE_IDENT_RE,relevance:0},E})},function(r){r.exports=function(_){var E='[A-Za-z$_][0-9A-Za-z$_]*',w={keyword:'in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as',literal:'true false null undefined NaN Infinity',built_in:'eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise'},C={className:'number',variants:[{begin:'\\b(0[bB][01]+)'},{begin:'\\b(0[oO][0-7]+)'},{begin:_.C_NUMBER_RE}],relevance:0},R={className:'subst',begin:'\\$\\{',end:'\\}',keywords:w,contains:[]},N={className:'string',begin:'`',end:'`',contains:[_.BACKSLASH_ESCAPE,R]};R.contains=[_.APOS_STRING_MODE,_.QUOTE_STRING_MODE,N,C,_.REGEXP_MODE];var O=R.contains.concat([_.C_BLOCK_COMMENT_MODE,_.C_LINE_COMMENT_MODE]);return{aliases:['js','jsx'],keywords:w,contains:[{className:'meta',relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},{className:'meta',begin:/^#!/,end:/$/},_.APOS_STRING_MODE,_.QUOTE_STRING_MODE,N,_.C_LINE_COMMENT_MODE,_.C_BLOCK_COMMENT_MODE,C,{begin:/[{,]\s*/,relevance:0,contains:[{begin:E+'\\s*:',returnBegin:!0,relevance:0,contains:[{className:'attr',begin:E,relevance:0}]}]},{begin:'('+_.RE_STARTERS_RE+'|\\b(case|return|throw)\\b)\\s*',keywords:'return throw case',contains:[_.C_LINE_COMMENT_MODE,_.C_BLOCK_COMMENT_MODE,_.REGEXP_MODE,{className:'function',begin:'(\\(.*?\\)|'+E+')\\s*=>',returnBegin:!0,end:'\\s*=>',contains:[{className:'params',variants:[{begin:E},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:w,contains:O}]}]},{begin://,subLanguage:'xml',contains:[{begin:/<\w+\s*\/>/,skip:!0},{begin:/<\w+/,end:/(\/\w+|\w+\/)>/,skip:!0,contains:[{begin:/<\w+\s*\/>/,skip:!0},'self']}]}],relevance:0},{className:'function',beginKeywords:'function',end:/\{/,excludeEnd:!0,contains:[_.inherit(_.TITLE_MODE,{begin:E}),{className:'params',begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:O}],illegal:/\[|%/},{begin:/\$[(.]/},_.METHOD_GUARD,{className:'class',beginKeywords:'class',end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:'extends'},_.UNDERSCORE_TITLE_MODE]},{beginKeywords:'constructor',end:/\{/,excludeEnd:!0}],illegal:/#(?!!)/}}},function(r,h){'use strict';h.__esModule=!0;var E=h.canUseDOM=!!('undefined'!=typeof window&&window.document&&window.document.createElement),w=h.addEventListener=function(A,P,T){return A.addEventListener?A.addEventListener(P,T,!1):A.attachEvent('on'+P,T)},S=h.removeEventListener=function(A,P,T){return A.removeEventListener?A.removeEventListener(P,T,!1):A.detachEvent('on'+P,T)},C=h.getConfirmation=function(A,P){return P(window.confirm(A))},R=h.supportsHistory=function(){var A=window.navigator.userAgent;return(-1!==A.indexOf('Android 2.')||-1!==A.indexOf('Android 4.0'))&&-1!==A.indexOf('Mobile Safari')&&-1===A.indexOf('Chrome')&&-1===A.indexOf('Windows Phone')?!1:window.history&&'pushState'in window.history},N=h.supportsPopStateOnHashChange=function(){return-1===window.navigator.userAgent.indexOf('Trident')},O=h.supportsGoWithoutReloadUsingHash=function(){return-1===window.navigator.userAgent.indexOf('Firefox')},I=h.isExtraneousPopstateEvent=function(A){return A.state===void 0&&-1===navigator.userAgent.indexOf('CriOS')}},function(r,h,_){'use strict';function E(A){return A&&A.__esModule?A:{default:A}}h.__esModule=!0,h.locationsAreEqual=h.createLocation=void 0;var w=Object.assign||function(A){for(var P=1,T;P>1,A=-7,P=w?C-1:0,T=w?-1:1,U=_[E+P];for(P+=T,R=U&(1<<-A)-1,U>>=-A,A+=O;0>=-A,A+=S;0>1,T=23===C?5.960464477539063e-8-6.617444900424222e-24:0,U=S?0:R-1,D=S?1:-1,B=0>E||0===E&&0>1/E?1:0;for(E=_Mathabs(E),isNaN(E)||E===Infinity?(O=isNaN(E)?1:0,N=A):(N=_Mathfloor(Math.log(E)/Math.LN2),1>E*(I=_Mathpow(2,-N))&&(N--,I*=2),E+=1<=N+P?T/I:T*_Mathpow(2,1-P),2<=E*I&&(N++,I/=2),N+P>=A?(O=0,N=A):1<=N+P?(O=(E*I-1)*_Mathpow(2,C),N+=P):(O=E*_Mathpow(2,P-1)*_Mathpow(2,C),N=0));8<=C;_[w+U]=255&O,U+=D,O/=256,C-=8);for(N=N<Ie.indexOf(Ae)&&(Me[Ae]=Oe[Ae]);return Me}function B(Oe,Ie){var Me=Oe.props||ce,Ae=Ie.props||ce,Pe=Y(Ae.path)-Y(Me.path);return Pe||(Ae.path&&Me.path?Ae.path.length-Me.path.length:0)}function W(Oe,Ie){return decodeURIComponent(0|Ie?Oe:Oe.replace('[]',''))}function H(Oe){return Oe.replace(/(^\/+|\/+$)/g,'')}function Y(Oe){return void 0===Oe&&(Oe=''),(H(Oe).match(/\/+/g)||'').length}function z(Oe,Ie){for(var Me=0,Ae;MeRn?Rn:Cn,On=0,In;OnRn)for(On=Nn;OnNn||Mn>On)break outer;jn=vn[In],Wn=_n[Mn],Wn.dom&&(_n[Mn]=Wn=J(Wn))}for(;Hn.key===Yn.key;){if(tt(Hn,Yn,xn,En,wn,Sn,kn),Nn--,On--,In>Nn||Mn>On)break outer;Hn=vn[Nn],Yn=_n[On],Yn.dom&&(_n[On]=Yn=J(Yn))}if(Hn.key===Wn.key){tt(Hn,Wn,xn,En,wn,Sn,kn),Re(xn,Wn.dom,jn.dom),Nn--,Mn++,Hn=vn[Nn],Wn=_n[Mn],Wn.dom&&(_n[Mn]=Wn=J(Wn));continue}if(jn.key===Yn.key){tt(jn,Yn,xn,En,wn,Sn,kn),Dn=On+1,Ln=Dn<_n.length?_n[Dn].dom:null,Re(xn,Yn.dom,Ln),In++,On--,jn=vn[In],Yn=_n[On],Yn.dom&&(_n[On]=Yn=J(Yn));continue}break}if(In>Nn){if(Mn<=On)for(Dn=On+1,Ln=Dn<_n.length?_n[Dn].dom:null;Mn<=On;)Bn=_n[Mn],Bn.dom&&(_n[Mn]=Bn=J(Bn)),Mn++,Re(xn,yt(Bn,null,En,wn,Sn),Ln);}else if(Mn>On)for(;In<=Nn;)he(vn[In++],xn,En,!1,kn);else{Cn=Nn-In+1,Rn=On-Mn+1;var zn=vn,Vn=Array(Rn);for(An=0;An=Rn||16>=Cn*Rn){for(An=In;An<=Nn;An++)if(Tn=vn[An],$nPn?Kn=!0:Gn=Pn,Un.dom&&(_n[Pn]=Un=J(Un)),tt(Tn,Un,xn,En,wn,Sn,kn),$n++,zn[An]=null;break}}else{var Fn=new Map;for(An=Mn;An<=On;An++)Bn=_n[An],Fn.set(Bn.key,An);for(An=In;An<=Nn;An++)Tn=vn[An],$nPn?Kn=!0:Gn=Pn,Un.dom&&(_n[Pn]=Un=J(Un)),tt(Tn,Un,xn,En,wn,Sn,kn),$n++,zn[An]=null))}if(Cn===vn.length&&0==$n)for(Ae(xn,vn,En,kn);MnPn||An!==Xn[Pn]?(Gn=An+Mn,Bn=_n[Gn],Dn=Gn+1,Ln=Dn<_n.length?_n[Dn].dom:null,Re(xn,Bn.dom,Ln)):Pn--}else if($n!==Rn)for(An=Rn-1;0<=An;An--)-1===Vn[An]&&(Gn=An+Mn,Bn=_n[Gn],Bn.dom&&(_n[Gn]=Bn=J(Bn)),Dn=Gn+1,Ln=Dn<_n.length?_n[Dn].dom:null,Re(xn,yt(Bn,null,En,wn,Sn),Ln))}}}function ut(vn){var _n=vn.slice(0),xn=[0],En,wn,Sn,kn,Cn;for(En=0;En ({\n col11: `Pinned left ${i}`,\n col1: i,\n col2: `Title ${i}`,\n col21: `Pinned right ${i}`,\n col3: \'Lorem Ipsum is simply dummy text of the printing and typesetting industry.\'\n}));\n\nexport default class Viewport extends Component {\n constructor() {\n super();\n\n this.state = {\n gridState: {\n columns: [{\n name: \'col1\',\n enableSorting: true\n }, {\n name: \'col11\',\n width: 120,\n pinnedLeft: true,\n enableResizing: true\n }, {\n name: \'col2\',\n minWidth: 60,\n enableMoving: true,\n enableFiltering: true,\n placeholder: \'Search\',\n width: 150,\n enableResizing: true\n }, {\n name: \'col21\',\n width: 120,\n pinnedRight: true,\n enableResizing: true\n }, {\n name: \'col3\',\n displayName: \'Column 3\',\n width: 200,\n maxWidth: 300,\n enableMoving: true,\n enableResizing: true\n }, {\n name: \'4\',\n width: \'50%\'\n }],\n rowState: {\n selectedIndex: 0\n }\n },\n data,\n originalData: data\n };\n\n this.callback = this.callback.bind(this);\n }\n\n callback(action) {\n console.log(action);\n switch (action.type) {\n case \'FILTER_COLUMN\':\n case \'SORT_COLUMN\':\n {\n const gridState = reducer(this.state.gridState, action);\n this.setState({\n gridState,\n data: selectGridData(gridState, this.state.originalData)\n });\n break;\n }\n\n default:\n this.setState({\n gridState: reducer(this.state.gridState, action)\n });\n break;\n }\n }\n\n render({}, { gridState, data }) {\n return createElement(TrackedGrid, {\n viewportWidth: 600,\n viewportHeight: 360,\n headerHeight: 60,\n columnComponent: undefined,\n rowComponent: undefined,\n state: gridState,\n data: data,\n callback: this.callback\n });\n }\n}'},function(r){r.exports='import createElement from \'inferno-create-element\';\nimport Component from \'inferno-component\';\nimport { withScrollProps, Grid } from \'../../index.js\';\n\nconst TrackedGrid = withScrollProps(Grid);\n\nconst gridState = {\n columns: [{\n name: \'col1\',\n width: \'20%\'\n }, {\n name: \'col2\',\n width: \'35%\'\n }, {\n name: \'col3\',\n width: \'45%\'\n }]\n};\n\nconst data = Array(100000).fill(0).map((item, i) => ({\n col1: i,\n col2: `Col2 ${i}`,\n col3: \'Lorem Ipsum is simply dummy text of the printing and typesetting industry.\'\n}));\n\nexport default class Minimal extends Component {\n render() {\n return createElement(TrackedGrid, {\n viewportWidth: 720,\n viewportHeight: 480,\n state: gridState,\n data: data });\n }\n}'},function(r){r.exports='import createElement from \'inferno-create-element\';\nimport { withScrollProps, Grid } from \'../../index.js\';\n\nconst TrackedGrid = withScrollProps(Grid);\n\nconst gridState = {\n columns: [{\n name: \'col1\',\n width: \'50%\'\n }, {\n name: \'col11\',\n width: 120,\n pinnedLeft: true\n }, {\n name: \'col2\',\n width: \'50%\'\n }, {\n name: \'col21\',\n width: 120,\n pinnedRight: true\n }]\n};\n\nconst data = Array(100000).fill(0).map((item, i) => ({\n col11: `Pinned left ${i}`,\n col1: i,\n col2: `Title ${i}`,\n col21: `Pinned right ${i}`,\n col3: \'Lorem Ipsum is simply dummy text of the printing and typesetting industry.\'\n}));\n\nexport default (() => createElement(TrackedGrid, {\n viewportWidth: 600,\n viewportHeight: 360,\n state: gridState,\n data: data }));'},function(r){'use strict';var E=function(R){return'/'===R.charAt(0)},w=function(R,N){for(var O=N,I=O+1,M=R.length;I=arguments.length||void 0===arguments[1]?'':arguments[1],O=R&&R.split('/')||[],I=N&&N.split('/')||[],M=R&&E(R),A=N&&E(N),P=M||A;if(R&&E(R)?I=O:O.length&&(I.pop(),I=I.concat(O)),!I.length)return'/';var T;if(I.length){var U=I[I.length-1];T='.'===U||'..'===U||''===U}else T=!1;for(var D=0,B=I.length,W;0<=B;B--)W=I[B],'.'===W?w(I,B):'..'===W?(w(I,B),D++):D&&(w(I,B),D--);if(!P)for(;D--;D)I.unshift('..');!P||''===I[0]||I[0]&&E(I[0])||I.unshift('');var H=I.join('/');return T&&'/'!==H.substr(-1)&&(H+='/'),H}},function(r,h,_){var E=_(50);'string'==typeof E&&(E=[[r.i,E,'']]);_(9)(E,{});E.locals&&(r.exports=E.locals),!1},function(r,h,_){var E=_(51);'string'==typeof E&&(E=[[r.i,E,'']]);_(9)(E,{});E.locals&&(r.exports=E.locals),!1},function(r,h,_){var E=_(52);'string'==typeof E&&(E=[[r.i,E,'']]);_(9)(E,{});E.locals&&(r.exports=E.locals),!1},function(r,h){'use strict';h.__esModule=!0;var E='function'==typeof Symbol&&'symbol'==typeof Symbol.iterator?function(S){return typeof S}:function(S){return S&&'function'==typeof Symbol&&S.constructor===Symbol&&S!==Symbol.prototype?'symbol':typeof S};h.default=function S(C,R){if(C===R)return!0;if(null==C||null==R)return!1;if(Array.isArray(C))return Array.isArray(R)&&C.length===R.length&&C.every(function(T,U){return S(T,R[U])});var N='undefined'==typeof C?'undefined':E(C),O='undefined'==typeof R?'undefined':E(R);if(N!==O)return!1;if('object'===N){var I=C.valueOf(),M=R.valueOf();if(I!==C||M!==R)return S(I,M);var A=Object.keys(C),P=Object.keys(R);return A.length===P.length&&A.every(function(T){return S(C[T],R[T])})}return!1}},function(r){var _=function(){return this}();try{_=_||Function('return this')()||(1,eval)('this')}catch(E){'object'==typeof window&&(_=window)}r.exports=_},function(r,h,_){'use strict';Object.defineProperty(h,'__esModule',{value:!0});var E=_(4),w=_.n(E),S=_(0),C=_.n(S),R=_(20),N=_.n(R),O=_(19),I=_.n(O),M=_(17),A=_(18),P=_(10);const T=P.a.map((U)=>Object.assign({},U,{component:()=>C()(A.a,U)}));w.a.render(C()(R.Router,{history:I()()},C()(R.Route,{component:M.a},T.map((U)=>C()(R.Route,{key:U.href,path:U.href,component:U.component})))),document.body.appendChild(document.createElement('div')))}]); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Webpack App 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@trysound/datagrid", 3 | "version": "0.10.0", 4 | "main": "dist/datagrid.js", 5 | "files": [ 6 | "dist" 7 | ], 8 | "repository": "trysound/datagrid", 9 | "author": "Bogdan Chadkin ", 10 | "license": "MIT", 11 | "scripts": { 12 | "build": "rollup -c", 13 | "build-example": "cross-env NODE_ENV=production webpack", 14 | "start": "webpack-dev-server --hot --open", 15 | "test": "cross-env BABEL_ENV=test jest --coverage", 16 | "posttest": "eslint src --env jest", 17 | "start-test": "yarn test -- --watchAll", 18 | "prepublish": "rollup -c" 19 | }, 20 | "jest": { 21 | "coverageReporters": [ 22 | "text-summary", 23 | "html" 24 | ] 25 | }, 26 | "babel": { 27 | "plugins": [ 28 | [ 29 | "transform-react-jsx", 30 | { 31 | "useBuiltIns": true 32 | } 33 | ] 34 | ], 35 | "env": { 36 | "test": { 37 | "plugins": [ 38 | "transform-es2015-modules-commonjs" 39 | ] 40 | } 41 | } 42 | }, 43 | "devDependencies": { 44 | "babel-core": "^6.23.1", 45 | "babel-jest": "^19.0.0", 46 | "babel-loader": "^6.3.2", 47 | "babel-plugin-transform-es2015-modules-commonjs": "^6.24.0", 48 | "babel-plugin-transform-react-jsx": "^6.23.0", 49 | "babili-webpack-plugin": "^0.0.11", 50 | "cross-env": "^4.0.0", 51 | "css-loader": "^0.28.0", 52 | "eslint": "^3.19.0", 53 | "eslint-loader": "^1.7.1", 54 | "eslint-plugin-react": "^6.10.3", 55 | "highlight.js": "^9.10.0", 56 | "html-webpack-plugin": "^2.26.0", 57 | "jest-cli": "^19.0.2", 58 | "raw-loader": "^0.5.1", 59 | "react-router-dom": "^4.0.0", 60 | "rollup": "^0.41.5", 61 | "rollup-plugin-babel": "^2.7.1", 62 | "style-loader": "^0.16.1", 63 | "webpack": "^2.3.3", 64 | "webpack-dev-server": "^2" 65 | }, 66 | "dependencies": { 67 | "react": "^15.4.2", 68 | "react-dom": "^15.4.2", 69 | "recompose": "^0.22.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | 3 | const pkg = require('./package.json'); 4 | 5 | export default { 6 | entry: './src/index.js', 7 | dest: pkg.main, 8 | format: 'cjs', 9 | external: Object.keys(pkg.dependencies), 10 | plugins: [ 11 | babel() 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /src/DefaultColumn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withHandlers } from 'recompose'; 3 | import { filterColumn, sortColumn } from './actionCreators.js'; 4 | 5 | const border = '1px solid #d4d4d4'; 6 | const activeBorder = '1px solid #000'; 7 | 8 | const getColumnStyle = (gridState, column, last, ghost) => ({ 9 | position: 'relative', 10 | height: 'inherit', 11 | boxSizing: 'border-box', 12 | padding: '0 8px', 13 | borderTop: border, 14 | borderBottom: border, 15 | borderLeft: ghost && border 16 | || (gridState.moving && gridState.moving.right === column.name ? activeBorder : border) 17 | || '', 18 | 19 | borderRight: ghost && border 20 | || last && (gridState.moving && gridState.moving.left === column.name ? activeBorder : border) 21 | || '', 22 | background: 'linear-gradient(to top, #eeeeee, #ffffff)', 23 | opacity: ghost ? .8 : 1 24 | }); 25 | 26 | const getInputStyle = () => ({ 27 | width: '100%', 28 | boxSizing: 'border-box', 29 | padding: '0 8px', 30 | border: 0, 31 | borderBottom: border, 32 | marginBottom: 8 33 | }); 34 | 35 | const Arrow = ({ direction }) => ( 36 |
41 |
42 | ); 43 | 44 | export default withHandlers({ 45 | onSortClink: props => () => props.callback(sortColumn(props.column.name)), 46 | onFilterInput: props => event => props.callback(filterColumn(props.column.name, event.target.value)) 47 | })(props => 48 |
49 |
50 | {props.column.displayName || props.column.name} 51 | {props.column.sort && 52 | 53 | } 54 |
55 | {props.column.enableFiltering && 56 | 60 | } 61 |
62 | ); 63 | -------------------------------------------------------------------------------- /src/DefaultRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withHandlers } from 'recompose'; 3 | import { selectRow } from './actionCreators.js'; 4 | 5 | const border = '1px solid #d4d4d4'; 6 | 7 | const getRowStyle = (gridState, index) => ({ 8 | display: 'flex', 9 | height: 'inherit', 10 | cursor: 'default', 11 | background: gridState.selectedIndex === index ? '#c9dde1' : index % 2 === 0 ? '#fff' : '#f3f3f3' 12 | }); 13 | 14 | const getColumnStyle = (column, last) => ({ 15 | flexShrink: 0, 16 | display: 'flex', 17 | alignItems: 'center', 18 | width: column.width, 19 | padding: '0 8px', 20 | boxSizing: 'border-box', 21 | borderLeft: border, 22 | borderRight: last ? border : '' 23 | }); 24 | 25 | const DefaultRow = withHandlers({ 26 | selectRow: props => () => props.callback(selectRow(props.index)) 27 | })(props => 28 |
29 | {props.columns.map((item, columnIndex) => 30 |
31 |
32 | {props.datum[item.name]} 33 |
34 |
35 | )} 36 |
37 | ); 38 | 39 | export default DefaultRow; 40 | -------------------------------------------------------------------------------- /src/Grid.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose, withPropsOnChange } from 'recompose'; 3 | import Header from './Header.js'; 4 | import ResizeGhost from './ResizeGhost.js'; 5 | import List from './List.js'; 6 | import { withDefaults, withPinnedColumns } from './hoc/index.js'; 7 | import { getVisibleRows } from './utils/index.js'; 8 | import { markMoveDest, moveColumn, resizeColumn } from './actionCreators.js'; 9 | 10 | export default compose( 11 | withDefaults(), 12 | withPropsOnChange(['transform', 'columns', 'gridState', 'data'], ({ transform, columns, gridState, data }) => ({ 13 | data: transform({ columns, gridState }, data) 14 | })), 15 | withPinnedColumns(), 16 | withPropsOnChange( 17 | ['columns'], 18 | ({ columns }) => ({ 19 | tableWidth: columns.reduce((acc, item) => acc + item.width, 0) 20 | }) 21 | ), 22 | withPropsOnChange(['gridState', 'columns', 'callback'], ({ gridState, columns, callback }) => ({ 23 | columnProps: { 24 | gridState, 25 | callback 26 | }, 27 | rowProps: { 28 | gridState, 29 | columns, 30 | callback 31 | } 32 | })), 33 | withPropsOnChange(['scrollTop', 'viewportHeight', 'headerHeight', 'rowHeight', 'data'], props => { 34 | const [start, end] = getVisibleRows({ 35 | scrollTop: props.scrollTop - props.headerHeight, 36 | viewportHeight: props.viewportHeight - props.headerHeight, 37 | rowHeight: props.rowHeight, 38 | rowsCount: props.data.length 39 | }); 40 | return { 41 | start, 42 | end 43 | }; 44 | }) 45 | )(class GridWrapper extends React.Component { 46 | constructor(props) { 47 | super(props); 48 | this.state = { 49 | ghost: false, 50 | ghostX: 0 51 | }; 52 | this.onResizing = this.onResizing.bind(this); 53 | this.onResize = this.onResize.bind(this); 54 | this.onMoving = this.onMoving.bind(this); 55 | this.onMove = this.onMove.bind(this); 56 | } 57 | 58 | onResizing(name, ghostX) { 59 | this.setState({ 60 | ghost: true, 61 | ghostX 62 | }); 63 | } 64 | 65 | onResize(name, columnWidth) { 66 | this.setState({ 67 | ghost: false 68 | }); 69 | this.props.callback(resizeColumn(name, columnWidth)); 70 | } 71 | 72 | onMoving(name, left, right) { 73 | if (name !== this.movingName || left !== this.movingLeft || right !== this.movingRight) { 74 | this.movingName = name; 75 | this.movingLeft = left; 76 | this.movingRight = right; 77 | this.props.callback(markMoveDest(name, left, right)); 78 | } 79 | } 80 | 81 | onMove(name, left, right) { 82 | this.movingName = null; 83 | this.movingLeft = null; 84 | this.movingRight = null; 85 | this.props.callback(moveColumn(name, left, right)); 86 | } 87 | 88 | render() { 89 | return ( 90 |
91 | {Boolean(this.props.headerHeight) && 92 |
93 |
101 |
102 | } 103 | {this.state.ghost && } 104 | 111 |
112 | ); 113 | } 114 | }); 115 | -------------------------------------------------------------------------------- /src/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { bisectColumns, findColumn, trimColumnWidth } from './utils/index.js'; 3 | import { draggable } from './hoc/index.js'; 4 | 5 | const dragOffset = 3; 6 | 7 | const Container = ({ children }) => ( 8 |
9 | {children} 10 |
11 | ); 12 | 13 | const Resizer = ({ last }) => ( 14 |
23 |
24 | ); 25 | 26 | const ColumnWrapper = ({ column, index, last, columnComponent: Column, columnProps }) => ( 27 |
28 | 29 | {column.enableResizing && } 30 |
31 | ); 32 | 33 | const ColumnGhost = ({ x, column, index, columnComponent: Column, columnProps }) => ( 34 |
35 | 36 |
37 | ); 38 | 39 | const dragMove = ({ columns, x, dx, onResizing, onMoving }) => { 40 | const [startIndex, startX] = findColumn(columns, x - dx); 41 | const startColumn = columns[startIndex]; 42 | if (Math.abs(startX) <= dragOffset) { 43 | // resize previous 44 | // skip first to not conflict with pinned tables with moving 45 | if (startIndex !== 0) { 46 | const prevColumn = columns[startIndex - 1]; 47 | if (prevColumn.enableResizing) { 48 | onResizing(prevColumn.name, x); 49 | } 50 | } 51 | } else if (Math.abs(startX - startColumn.width) <= dragOffset) { 52 | // resize current 53 | if (startColumn.enableResizing) { 54 | onResizing(startColumn.name, x); 55 | } 56 | } else if (startColumn.enableMoving) { 57 | // move current 58 | const [leftIndex, rightIndex] = bisectColumns(columns, x - startX); 59 | onMoving( 60 | startColumn.name, 61 | leftIndex === -1 ? null : columns[leftIndex].name, 62 | rightIndex === -1 ? null : columns[rightIndex].name 63 | ); 64 | return { 65 | moving: true, 66 | movingPosition: x - startX, 67 | movingColumn: startColumn, 68 | movingIndex: startIndex 69 | }; 70 | } 71 | }; 72 | 73 | const dragEnd = ({ columns, x, dx, onResize, onMove }) => { 74 | const [startIndex, startX] = findColumn(columns, x - dx); 75 | const startColumn = columns[startIndex]; 76 | if (Math.abs(startX) <= dragOffset) { 77 | // resize previous 78 | // skip first to not conflict with pinned tables with moving 79 | if (startIndex !== 0) { 80 | const prevColumn = columns[startIndex - 1]; 81 | if (prevColumn.enableResizing) { 82 | onResize(prevColumn.name, trimColumnWidth(prevColumn, prevColumn.width + startX + dx)); 83 | } 84 | } 85 | } else if (Math.abs(startX - startColumn.width) <= dragOffset) { 86 | // resize current 87 | if (startColumn.enableResizing) { 88 | onResize(startColumn.name, trimColumnWidth(startColumn, startX + dx)); 89 | } 90 | } else if (startColumn.enableMoving) { 91 | // move current 92 | const [leftIndex, rightIndex] = bisectColumns(columns, x - startX); 93 | onMove( 94 | startColumn.name, 95 | leftIndex === -1 ? null : columns[leftIndex].name, 96 | rightIndex === -1 ? null : columns[rightIndex].name 97 | ); 98 | return { 99 | moving: false 100 | }; 101 | } 102 | }; 103 | 104 | export default draggable({ 105 | offset: dragOffset, 106 | style: { height: 'inherit'} 107 | })(class Header extends React.Component { 108 | constructor(props) { 109 | super(props); 110 | this.state = {}; 111 | } 112 | 113 | componentWillReceiveProps(nextProps) { 114 | if (this.props.dragging && this.props.x !== nextProps.x) { 115 | this.setState(dragMove(nextProps)); 116 | } 117 | if (!nextProps.dragging && this.props.dragging !== nextProps.dragging) { 118 | this.setState(dragEnd(nextProps)); 119 | } 120 | } 121 | 122 | render() { 123 | return ( 124 | 125 | {this.props.columns.map((column, index) => 126 | 133 | )} 134 | {this.state.moving && 135 | 141 | } 142 | 143 | ); 144 | } 145 | }); 146 | -------------------------------------------------------------------------------- /src/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shouldUpdate } from 'recompose'; 3 | import { withMiddleState } from './hoc/index.js'; 4 | import { compose, checkProps, getKeysByIndex } from './utils/index.js'; 5 | 6 | export default compose( 7 | shouldUpdate(checkProps('start', 'end', 'data', 'rowHeight', 'rowComponent', 'rowProps')), 8 | withMiddleState((props, state = {}) => ({ 9 | keys: getKeysByIndex(state.keys, props.start, props.end) 10 | })) 11 | )(({ data, rowHeight, start, end, keys, rowComponent: Row, rowProps }) => 12 |
13 |
14 | {data.slice(start, end + 1).map((datum, index) => 15 |
16 | 17 |
18 | )} 19 |
20 |
21 | ); 22 | -------------------------------------------------------------------------------- /src/List.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from './utils/testUtils.js'; 2 | import List from './List.js'; 3 | 4 | test('passes datum and index or row to component', () => { 5 | const result = []; 6 | mount(List).setProps({ 7 | start: 0, 8 | end: 1, 9 | rowHeight: 30, 10 | data: ['data1', 'data2'], 11 | rowComponent: props => { 12 | result.push(props); 13 | return null; 14 | } 15 | }); 16 | expect(result).toEqual([ 17 | { datum: 'data1', index: 0 }, 18 | { datum: 'data2', index: 1 } 19 | ]); 20 | }); 21 | 22 | test('container height is a sum of all rows', () => { 23 | const { setProps, getWrapper } = mount(List); 24 | setProps({ 25 | start: 0, 26 | end: 3, 27 | rowHeight: 30, 28 | data: ['data1', 'data2'], 29 | rowComponent: () => null 30 | }); 31 | expect(getWrapper().children[0].style.height).toEqual('60px'); 32 | }); 33 | 34 | test('each row wrapper should have height', () => { 35 | const { setProps, getWrapper } = mount(List); 36 | setProps({ 37 | start: 0, 38 | end: 3, 39 | rowHeight: 30, 40 | data: ['data1', 'data2'], 41 | rowComponent: () => null 42 | }); 43 | const rows = getWrapper().children[0].children[0].children; 44 | expect(rows[0].style.height).toEqual('30px'); 45 | expect(rows[1].style.height).toEqual('30px'); 46 | }); 47 | 48 | test('passes rowProps to rowComponent', () => { 49 | const results = []; 50 | mount(List).setProps({ 51 | start: 0, 52 | end: 3, 53 | rowHeight: 30, 54 | data: ['data1', 'data2'], 55 | rowComponent: props => { 56 | results.push(props); 57 | return null; 58 | }, 59 | rowProps: { a: 'a', b: 'b' } 60 | }); 61 | expect(results).toEqual([ 62 | { 63 | datum: 'data1', 64 | index: 0, 65 | a: 'a', 66 | b: 'b' 67 | }, 68 | { 69 | datum: 'data2', 70 | index: 1, 71 | a: 'a', 72 | b: 'b' 73 | } 74 | ]); 75 | }); 76 | -------------------------------------------------------------------------------- /src/ResizeGhost.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ({ x }) => ( 4 |
13 |
14 | ); 15 | -------------------------------------------------------------------------------- /src/actionCreators.js: -------------------------------------------------------------------------------- 1 | export const markMoveDest = (name, left, right) => ({ 2 | type: 'MARK_MOVE_DEST', 3 | name, 4 | left, 5 | right 6 | }); 7 | 8 | export const moveColumn = (name, left, right) => ({ 9 | type: 'MOVE_COLUMN', 10 | name, 11 | left, 12 | right 13 | }); 14 | 15 | export const resizeColumn = (name, size) => ({ 16 | type: 'RESIZE_COLUMN', 17 | name, 18 | size 19 | }); 20 | 21 | export const filterColumn = (name, filter) => ({ 22 | type: 'FILTER_COLUMN', 23 | name, 24 | filter 25 | }); 26 | 27 | export const sortColumn = name => ({ 28 | type: 'SORT_COLUMN', 29 | name 30 | }); 31 | 32 | export const selectRow = rowIndex => ({ 33 | type: 'SELECT_ROW', 34 | rowIndex 35 | }); 36 | -------------------------------------------------------------------------------- /src/examples/App.css: -------------------------------------------------------------------------------- 1 | .App__viewport { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | overflow: auto; 8 | } 9 | 10 | .App__container { 11 | position: absolute; 12 | width: 100%; 13 | min-height: 100vh; 14 | display: flex; 15 | align-items: stretch; 16 | font-family: sans-serif; 17 | } 18 | 19 | .App__nav { 20 | flex-shrink: 0; 21 | box-sizing: border-box; 22 | width: 300px; 23 | padding: 15px; 24 | background: #eee; 25 | } 26 | 27 | .App__link { 28 | display: block; 29 | padding: 4px 0; 30 | color: #666; 31 | } 32 | 33 | .App__link:hover { 34 | color: #000; 35 | } 36 | 37 | .App__main { 38 | flex: 1; 39 | } 40 | -------------------------------------------------------------------------------- /src/examples/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import nav from './nav.js'; 4 | import './App.css'; 5 | 6 | export default props => ( 7 |
8 |
9 |
10 | {nav.map(item => 11 | 12 | {item.title} 13 | 14 | )} 15 |
16 |
17 | {props.children} 18 |
19 |
20 |
21 | ); 22 | -------------------------------------------------------------------------------- /src/examples/Code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import hljs from 'highlight.js/lib/highlight.js'; 3 | import javascript from 'highlight.js/lib/languages/javascript.js'; 4 | import 'highlight.js/styles/monokai.css'; 5 | 6 | hljs.registerLanguage('javascript', javascript); 7 | 8 | export default class Code extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.html = hljs.highlightAuto(props.value).value; 12 | } 13 | 14 | componentWillReceiveProps(nextProps) { 15 | if (this.props.value !== nextProps.value) { 16 | this.html = hljs.highlightAuto(nextProps.value).value; 17 | } 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 |                 
24 |             
25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/examples/Page.css: -------------------------------------------------------------------------------- 1 | .Page__container {} 2 | 3 | .Page__header { 4 | padding: 15px; 5 | background: #ddd; 6 | font-size: 2em; 7 | } 8 | 9 | .Page__content { 10 | display: flex; 11 | padding: 15px; 12 | } 13 | -------------------------------------------------------------------------------- /src/examples/Page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Code from './Code.js'; 3 | import './Page.css'; 4 | 5 | export default props => ( 6 |
7 |
{props.title}
8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 | ); 18 | -------------------------------------------------------------------------------- /src/examples/demos/GridDemo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, reducer, withScrollProps } from '../../index.js'; 3 | 4 | const TrackedGrid = withScrollProps(Grid); 5 | 6 | const data = Array(100000).fill(0).map((item, i) => ({ 7 | col11: `Pinned left ${i}`, 8 | col1: i, 9 | col2: `Title ${i}`, 10 | col21: `Pinned right ${i}`, 11 | col3: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.' 12 | })); 13 | 14 | export default class Viewport extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = { 19 | columns: [ 20 | { 21 | name: 'col1', 22 | enableSorting: true 23 | }, 24 | { 25 | name: 'col11', 26 | width: 120, 27 | pinnedLeft: true, 28 | enableResizing: true 29 | }, 30 | { 31 | name: 'col2', 32 | minWidth: 60, 33 | enableMoving: true, 34 | enableFiltering: true, 35 | placeholder: 'Search', 36 | width: 150, 37 | enableResizing: true 38 | }, 39 | { 40 | name: 'col21', 41 | width: 120, 42 | pinnedRight: true, 43 | enableResizing: true 44 | }, 45 | { 46 | name: 'col3', 47 | displayName: 'Column 3', 48 | width: 200, 49 | maxWidth: 300, 50 | enableMoving: true, 51 | enableResizing: true 52 | }, 53 | { 54 | name: '4', 55 | width: '50%' 56 | } 57 | ], 58 | gridState: { 59 | selectedIndex: 0 60 | }, 61 | data 62 | }; 63 | 64 | this.callback = this.callback.bind(this); 65 | } 66 | 67 | callback(action) { 68 | console.log(action); 69 | switch (action.type) { 70 | default: 71 | this.setState(state => reducer(state, action)); 72 | break; 73 | } 74 | } 75 | 76 | render() { 77 | return ( 78 | 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/examples/demos/Minimal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withScrollProps, Grid } from '../../index.js'; 3 | 4 | const TrackedGrid = withScrollProps(Grid); 5 | 6 | const columns = [ 7 | { 8 | name: 'col1', 9 | width: '20%' 10 | }, 11 | { 12 | name: 'col2', 13 | width: '35%' 14 | }, 15 | { 16 | name: 'col3', 17 | width: '45%' 18 | } 19 | ]; 20 | 21 | const data = Array(100000).fill(0).map((item, i) => ({ 22 | col1: i, 23 | col2: `Col2 ${i}`, 24 | col3: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.' 25 | })); 26 | 27 | export default () => ( 28 | 33 | ); 34 | -------------------------------------------------------------------------------- /src/examples/demos/PinnedColumns.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withScrollProps, Grid } from '../../index.js'; 3 | 4 | const TrackedGrid = withScrollProps(Grid); 5 | 6 | const columns = [ 7 | { 8 | name: 'col1', 9 | width: '50%' 10 | }, 11 | { 12 | name: 'col11', 13 | width: 120, 14 | pinnedLeft: true 15 | }, 16 | { 17 | name: 'col2', 18 | width: '50%' 19 | }, 20 | { 21 | name: 'col21', 22 | width: 120, 23 | pinnedRight: true 24 | } 25 | ]; 26 | 27 | const data = Array(100000).fill(0).map((item, i) => ({ 28 | col11: `Pinned left ${i}`, 29 | col1: i, 30 | col2: `Title ${i}`, 31 | col21: `Pinned right ${i}`, 32 | col3: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.' 33 | })); 34 | 35 | export default () => ( 36 | 41 | ); 42 | -------------------------------------------------------------------------------- /src/examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { HashRouter as Router, Route } from 'react-router-dom'; 4 | import App from './App.js'; 5 | import Page from './Page.js'; 6 | import nav from './nav.js'; 7 | 8 | const navLinked = nav.map(item => Object.assign({}, item, { 9 | component: () => 10 | })); 11 | 12 | ReactDOM.render( 13 | 14 | 15 | {navLinked.map(item => 16 | 17 | )} 18 | 19 | , 20 | document.body.appendChild(document.createElement('div')) 21 | ); 22 | -------------------------------------------------------------------------------- /src/examples/nav.js: -------------------------------------------------------------------------------- 1 | import Minimal from './demos/Minimal.js'; 2 | import minimalCode from 'raw-loader!./demos/Minimal.js'; 3 | import PinnedColumns from './demos/PinnedColumns.js'; 4 | import pinnedColumnsCode from 'raw-loader!./demos/PinnedColumns.js'; 5 | import GridDemo from './demos/GridDemo.js'; 6 | import gridDemoCode from 'raw-loader!./demos/GridDemo.js'; 7 | 8 | export default [ 9 | { 10 | href: '/minimal', 11 | title: 'Minimal', 12 | component: Minimal, 13 | code: minimalCode 14 | }, 15 | { 16 | href: '/pinned-columns', 17 | title: 'Pinned columns', 18 | component: PinnedColumns, 19 | code: pinnedColumnsCode 20 | }, 21 | { 22 | href: '/complex', 23 | title: 'Complex demo', 24 | component: GridDemo, 25 | code: gridDemoCode 26 | } 27 | ]; 28 | -------------------------------------------------------------------------------- /src/hoc/draggable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ({ offset = 3, style = {} } = {}) => BaseComponent => class extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | dragging: false 8 | }; 9 | this.ref = this.ref.bind(this); 10 | this.onMouseDown = this.onMouseDown.bind(this); 11 | } 12 | 13 | ref(element) { 14 | this.element = element; 15 | } 16 | 17 | onMouseDown(downEvent) { 18 | const clientX = this.element.getBoundingClientRect().left; 19 | const startX = downEvent.clientX - clientX; 20 | const onMouseMove = e => { 21 | e.preventDefault(); 22 | const x = e.clientX - clientX; 23 | const dx = x - startX; 24 | if (this.state.dragging || Math.abs(dx) > offset) { 25 | this.setState({ 26 | dragging: true, 27 | dx, 28 | x 29 | }); 30 | } 31 | }; 32 | const onMouseUp = e => { 33 | document.removeEventListener('mousemove', onMouseMove); 34 | document.removeEventListener('mouseup', onMouseUp); 35 | if (this.state.dragging || Math.abs(e.clientX - clientX - startX) > offset) { 36 | this.setState({ 37 | dragging: false 38 | }); 39 | } 40 | }; 41 | document.addEventListener('mousemove', onMouseMove); 42 | document.addEventListener('mouseup', onMouseUp); 43 | } 44 | 45 | render() { 46 | return ( 47 |
48 | 49 |
50 | ); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/hoc/index.js: -------------------------------------------------------------------------------- 1 | // compose 2 | export { default as withMiddleState } from './withMiddleState.js'; 3 | 4 | // behavior 5 | export { default as withScrollProps } from './withScrollProps.js'; 6 | export { default as draggable } from './draggable.js'; 7 | export { default as withDefaults } from './withDefaults.js'; 8 | export { default as withPinnedColumns } from './withPinnedColumns.js'; 9 | -------------------------------------------------------------------------------- /src/hoc/withDefaults.js: -------------------------------------------------------------------------------- 1 | import { compose, withProps, withPropsOnChange } from 'recompose'; 2 | import { transform } from '../utils/index.js'; 3 | import DefaultColumn from '../DefaultColumn.js'; 4 | import DefaultRow from '../DefaultRow.js'; 5 | 6 | const defaultState = {}; 7 | const defaultMinWidth = 60; 8 | 9 | const parseWidth = (width, viewportWidth) => { 10 | if (typeof width === 'string' && width[width.length - 1] === '%') { 11 | return viewportWidth * Number(width.slice(0, -1)) / 100; 12 | } 13 | return width; 14 | }; 15 | 16 | const transformStub = data => data; 17 | 18 | export default () => compose( 19 | withProps(props => ({ 20 | headerHeight: props.headerHeight || 0, 21 | rowHeight: props.rowHeight || 30, 22 | gridState: props.gridState || defaultState 23 | })), 24 | withProps(props => ({ 25 | columnComponent: props.columnComponent || DefaultColumn, 26 | rowComponent: props.rowComponent || DefaultRow 27 | })), 28 | withPropsOnChange(['columns', 'viewportWidth'], props => ({ 29 | columns: props.columns.map(column => Object.assign({}, column, { 30 | width: parseWidth(column.width, props.viewportWidth) 31 | })) 32 | })), 33 | withPropsOnChange(['columns'], props => ({ 34 | columns: props.columns.map(column => Object.assign({}, column, { 35 | minWidth: column.minWidth || defaultMinWidth, 36 | width: Math.max(column.width || 0, column.minWidth || defaultMinWidth) 37 | })) 38 | })), 39 | withPropsOnChange(['transform'], props => ({ 40 | transform: 41 | typeof props.transform === 'function' && props.transform || 42 | props.transform === false && transformStub || 43 | transform 44 | })) 45 | ); 46 | -------------------------------------------------------------------------------- /src/hoc/withDefaults.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../utils/testUtils.js'; 2 | import withDefaults from './withDefaults.js'; 3 | import DefaultColumn from '../DefaultColumn.js'; 4 | import DefaultRow from '../DefaultRow.js'; 5 | import { transform } from '../utils'; 6 | 7 | const defaultWidth = 60; 8 | 9 | test('does not spread columns and gridState props', () => { 10 | const results = []; 11 | const wrapped = withDefaults()(props => { 12 | results.push(props); 13 | return null; 14 | }); 15 | const { setProps } = mount(wrapped); 16 | setProps({ 17 | columns: [], 18 | state: { 19 | columns: [1], 20 | gridState: {}, 21 | rowState: {} 22 | } 23 | }); 24 | expect(results[0].columns).toEqual([]); 25 | expect(results[0].columnState).toEqual(undefined); 26 | expect(results[0].rowState).toEqual(undefined); 27 | }); 28 | 29 | test('sets default gridState as an empty object', () => { 30 | const results = []; 31 | const wrapped = withDefaults()(props => { 32 | results.push(props); 33 | return null; 34 | }); 35 | const { setProps } = mount(wrapped); 36 | setProps({ 37 | columns: [] 38 | }); 39 | expect(results[0].gridState).toEqual({}); 40 | }); 41 | 42 | test('sets default columnComponent and rowComponent', () => { 43 | const results = []; 44 | const wrapped = withDefaults()(props => { 45 | results.push(props); 46 | return null; 47 | }); 48 | const { setProps } = mount(wrapped); 49 | setProps({ 50 | columns: [] 51 | }); 52 | expect(results[0].columnComponent).toEqual(DefaultColumn); 53 | expect(results[0].rowComponent).toEqual(DefaultRow); 54 | }); 55 | 56 | test('sets default column width', () => { 57 | const results = []; 58 | const wrapped = withDefaults()(props => { 59 | results.push(props); 60 | return null; 61 | }); 62 | const { setProps } = mount(wrapped); 63 | setProps({ 64 | columns: [ 65 | { 66 | name: 'col1' 67 | }, 68 | { 69 | name: 'col2', 70 | minWidth: 120 71 | }, 72 | { 73 | name: 'col3', 74 | width: 220 75 | }, 76 | { 77 | name: 'col4', 78 | minWidth: 120, 79 | width: 220 80 | }, 81 | { 82 | name: 'col5', 83 | minWidth: 120, 84 | width: 100 85 | } 86 | ] 87 | }); 88 | expect(results[0].columns).toEqual([ 89 | { 90 | name: 'col1', 91 | minWidth: defaultWidth, 92 | width: defaultWidth 93 | }, 94 | { 95 | name: 'col2', 96 | minWidth: 120, 97 | width: 120 98 | }, 99 | { 100 | name: 'col3', 101 | minWidth: defaultWidth, 102 | width: 220 103 | }, 104 | { 105 | name: 'col4', 106 | minWidth: 120, 107 | width: 220 108 | }, 109 | { 110 | name: 'col5', 111 | minWidth: 120, 112 | width: 120 113 | } 114 | ]); 115 | }); 116 | 117 | test('converts percentage width to number', () => { 118 | const results = []; 119 | const wrapped = withDefaults()(props => { 120 | results.push(props); 121 | return null; 122 | }); 123 | const { setProps } = mount(wrapped); 124 | setProps({ 125 | viewportWidth: 220, 126 | columns: [ 127 | { 128 | name: 'col1', 129 | width: '50%' 130 | }, 131 | { 132 | name: 'col2', 133 | width: '120' 134 | } 135 | ] 136 | }); 137 | expect(results[0].columns).toEqual([ 138 | { 139 | name: 'col1', 140 | minWidth: defaultWidth, 141 | width: 110 142 | }, 143 | { 144 | name: 'col2', 145 | minWidth: defaultWidth, 146 | width: 120 147 | } 148 | ]); 149 | }); 150 | 151 | test('sets default header and row heights', () => { 152 | const results = []; 153 | const wrapped = withDefaults()(props => { 154 | results.push(props); 155 | return null; 156 | }); 157 | const { setProps } = mount(wrapped); 158 | setProps({ 159 | columns: [] 160 | }); 161 | expect(results[0].headerHeight).toEqual(0); 162 | expect(results[0].rowHeight).toEqual(30); 163 | }); 164 | 165 | test('passes default data transform if nothing passes', () => { 166 | const results = []; 167 | const wrapped = withDefaults()(props => { 168 | results.push(props); 169 | return null; 170 | }); 171 | mount(wrapped).setProps({ 172 | columns: [] 173 | }); 174 | expect(results[0].transform).toBe(transform); 175 | }); 176 | 177 | test('does not change passed transform', () => { 178 | const results = []; 179 | const wrapped = withDefaults()(props => { 180 | results.push(props); 181 | return null; 182 | }); 183 | const customTransform = () => {}; 184 | mount(wrapped).setProps({ 185 | columns: [], 186 | transform: customTransform 187 | }); 188 | expect(results[0].transform).toBe(customTransform); 189 | }); 190 | 191 | test('pass stub transform on false', () => { 192 | const results = []; 193 | const wrapped = withDefaults()(props => { 194 | results.push(props); 195 | return null; 196 | }); 197 | mount(wrapped).setProps({ 198 | columns: [], 199 | transform: false 200 | }); 201 | const stub = []; 202 | expect(results[0].transform(stub)).toBe(stub); 203 | }); 204 | -------------------------------------------------------------------------------- /src/hoc/withMiddleState.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default reducer => BaseComponent => class extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = reducer(props); 7 | } 8 | 9 | componentWillReceiveProps(nextProps) { 10 | this.setState(reducer(nextProps, this.state)); 11 | } 12 | 13 | render() { 14 | return ; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/hoc/withPinnedColumns.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose, withPropsOnChange } from 'recompose'; 3 | 4 | export default () => BaseComponent => compose( 5 | withPropsOnChange(['columns'], props => ({ 6 | leftPinnedColumns: props.columns.filter(column => column.pinnedLeft), 7 | centerColumns: props.columns.filter(column => !column.pinnedLeft && !column.pinnedRight), 8 | rightPinnedColumns: props.columns.filter(column => column.pinnedRight) 9 | })) 10 | )(props => 11 |
12 | {props.leftPinnedColumns.length !== 0 && 13 |
14 | 15 |
16 | } 17 | 18 | {props.rightPinnedColumns.length !== 0 && 19 |
20 | 21 |
22 | } 23 |
24 | ); 25 | -------------------------------------------------------------------------------- /src/hoc/withPinnedColumns.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../utils/testUtils.js'; 2 | import withPinnedColumns from './withPinnedColumns.js'; 3 | 4 | describe('withPinnedColumns hoc', () => { 5 | it('splits columns to three groups', () => { 6 | const result = []; 7 | const wrapped = withPinnedColumns()(props => { 8 | result.push(props); 9 | return null; 10 | }); 11 | const { setProps } = mount(wrapped); 12 | setProps({ 13 | userParam: -1, 14 | columns: [ 15 | { name: '1' }, 16 | { name: '2', pinnedRight: true }, 17 | { name: '3', pinnedLeft: true }, 18 | { name: '4'}, 19 | { name: '5', pinnedLeft: true } 20 | ] 21 | }); 22 | expect(result.length).toEqual(3); 23 | expect(result[0].userParam).toEqual(-1); 24 | expect(result[0].columns).toEqual([ 25 | { name: '3', pinnedLeft: true }, 26 | { name: '5', pinnedLeft: true } 27 | ]); 28 | expect(result[1].userParam).toEqual(-1); 29 | expect(result[1].columns).toEqual([ 30 | { name: '1' }, 31 | { name: '4' } 32 | ]); 33 | expect(result[2].userParam).toEqual(-1); 34 | expect(result[2].columns).toEqual([ 35 | { name: '2', pinnedRight: true } 36 | ]); 37 | }); 38 | 39 | it('does not render left and right group without columns', () => { 40 | const result = []; 41 | const wrapped = withPinnedColumns()(props => { 42 | result.push(props); 43 | return null; 44 | }); 45 | const { setProps } = mount(wrapped); 46 | setProps({ 47 | columns: [ 48 | { name: '1' } 49 | ] 50 | }); 51 | expect(result.length).toEqual(1); 52 | expect(result[0].columns).toEqual([ 53 | { name: '1' } 54 | ]); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/hoc/withScrollProps.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default BaseComponent => class extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | scrollTop: 0, 8 | scrollLeft: 0, 9 | viewportWidth: props.viewportWidth, 10 | viewportHeight: props.viewportHeight 11 | }; 12 | this.ref = element => { 13 | this.element = element; 14 | if (element) { 15 | this.setState({ 16 | viewportWidth: element.clientWidth, 17 | viewportHeight: element.clientHeight 18 | }); 19 | } 20 | }; 21 | this.onScroll = e => this.setState({ 22 | scrollTop: e.target.scrollTop, 23 | scrollLeft: e.target.scrollLeft 24 | }); 25 | } 26 | 27 | componentWillReceiveProps(nextProps) { 28 | if (this.props.viewportWidth !== nextProps.viewportWidth || 29 | this.props.viewportHeight !== nextProps.viewportHeight 30 | ) { 31 | if (this.element) { 32 | this.setState({ 33 | viewportWidth: this.element.clientWidth, 34 | viewportHeight: this.element.clientHeight 35 | }); 36 | } 37 | } 38 | } 39 | 40 | render() { 41 | return ( 42 |
45 | 46 |
47 | ); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Grid } from './Grid.js'; 2 | export { default as DefaultColumn } from './DefaultColumn.js'; 3 | export { default as DefaultRow } from './DefaultRow.js'; 4 | export { default as reducer } from './reducers/index.js'; 5 | export { withScrollProps } from './hoc/index.js'; 6 | -------------------------------------------------------------------------------- /src/reducers/columns.js: -------------------------------------------------------------------------------- 1 | export default (state = [], action) => { 2 | switch (action.type) { 3 | case 'MOVE_COLUMN': 4 | if (action.left || action.right) { 5 | const index 6 | = action.left 7 | ? state.findIndex(item => item.name === action.left) + 1 8 | : state.findIndex(item => item.name === action.right); 9 | return [ 10 | ...state.slice(0, index).filter(item => item.name !== action.name), 11 | ...state.filter(item => item.name === action.name), 12 | ...state.slice(index).filter(item => item.name !== action.name) 13 | ]; 14 | } 15 | return state; 16 | 17 | case 'RESIZE_COLUMN': 18 | return state.map(item => { 19 | if (item.name === action.name) { 20 | return Object.assign({}, item, { 21 | width: action.size 22 | }); 23 | } 24 | return item; 25 | }); 26 | 27 | case 'FILTER_COLUMN': 28 | return state.map(item => { 29 | if (item.name === action.name) { 30 | return Object.assign({}, item, { 31 | filter: action.filter 32 | }); 33 | } 34 | return item; 35 | }); 36 | 37 | case 'SORT_COLUMN': 38 | return state.map(item => { 39 | if (item.name === action.name) { 40 | return Object.assign({}, item, { 41 | sort: !item.sort && 'asc' || item.sort === 'asc' && 'desc' || item.sort === 'desc' && null 42 | }); 43 | } 44 | if (item.sort) { 45 | return Object.assign({}, item, { 46 | sort: null 47 | }); 48 | } 49 | return item; 50 | }); 51 | 52 | default: 53 | return state; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/reducers/columns.test.js: -------------------------------------------------------------------------------- 1 | import reducer from './columns.js'; 2 | import { moveColumn, resizeColumn, filterColumn, sortColumn } from '../actionCreators.js'; 3 | 4 | describe('columns reducer', () => { 5 | it('moves column between left and right', () => { 6 | expect( 7 | reducer([ 8 | { name: 'col1' }, 9 | { name: 'col2' }, 10 | { name: 'col3' } 11 | ], moveColumn('col1', 'col2', 'col3')) 12 | ).toEqual([ 13 | { name: 'col2' }, 14 | { name: 'col1' }, 15 | { name: 'col3' } 16 | ]); 17 | }); 18 | 19 | it('moves column after left if right is null', () => { 20 | expect( 21 | reducer([ 22 | { name: 'col1' }, 23 | { name: 'col3' }, 24 | { name: 'col2' } 25 | ], moveColumn('col1', 'col3', null)) 26 | ).toEqual([ 27 | { name: 'col3' }, 28 | { name: 'col1' }, 29 | { name: 'col2' } 30 | ]); 31 | }); 32 | 33 | it('moves column after itself if right is null and column is the last', () => { 34 | expect( 35 | reducer([ 36 | { name: 'col1' }, 37 | { name: 'col2' } 38 | ], moveColumn('col2', null, 'col2')) 39 | ).toEqual([ 40 | { name: 'col1' }, 41 | { name: 'col2' } 42 | ]); 43 | }); 44 | 45 | it('moves column before right if left is null', () => { 46 | expect( 47 | reducer([ 48 | { name: 'col3' }, 49 | { name: 'col2' }, 50 | { name: 'col1' } 51 | ], moveColumn('col1', null, 'col2')) 52 | ).toEqual([ 53 | { name: 'col3' }, 54 | { name: 'col1' }, 55 | { name: 'col2' } 56 | ]); 57 | }); 58 | 59 | it('moves column before itself if left is null and column is the first', () => { 60 | expect( 61 | reducer([ 62 | { name: 'col1' }, 63 | { name: 'col2' } 64 | ], moveColumn('col1', null, 'col1')) 65 | ).toEqual([ 66 | { name: 'col1' }, 67 | { name: 'col2' } 68 | ]); 69 | }); 70 | 71 | it('does not move columns if left and right are null', () => { 72 | expect( 73 | reducer([ 74 | { name: 'col1' }, 75 | { name: 'col2' }, 76 | { name: 'col3' } 77 | ], moveColumn('col1', null, null)) 78 | ).toEqual([ 79 | { name: 'col1' }, 80 | { name: 'col2' }, 81 | { name: 'col3' } 82 | ]); 83 | }); 84 | 85 | it('changes width on resize column', () => { 86 | expect( 87 | reducer([ 88 | { 89 | name: 'col1', 90 | width: 100 91 | }, 92 | { 93 | name: 'col2', 94 | width: 150 95 | } 96 | ], resizeColumn('col1', 120)) 97 | ).toEqual([ 98 | { 99 | name: 'col1', 100 | width: 120 101 | }, 102 | { 103 | name: 'col2', 104 | width: 150 105 | } 106 | ]); 107 | }); 108 | 109 | it('adds filters to columns', () => { 110 | expect( 111 | reducer([ 112 | { 113 | name: 'col1' 114 | }, 115 | { 116 | name: 'col2' 117 | } 118 | ], filterColumn('col1', 'Text')) 119 | ).toEqual([ 120 | { 121 | name: 'col1', 122 | filter: 'Text' 123 | }, 124 | { 125 | name: 'col2' 126 | } 127 | ]); 128 | }); 129 | 130 | it('sorts colummn from null', () => { 131 | expect( 132 | reducer([ 133 | { 134 | name: 'col1' 135 | } 136 | ], sortColumn('col1')) 137 | ).toEqual([ 138 | { 139 | name: 'col1', 140 | sort: 'asc' 141 | } 142 | ]); 143 | }); 144 | 145 | it('sorts column from asc', () => { 146 | expect( 147 | reducer([ 148 | { 149 | name: 'col1', 150 | sort: 'asc' 151 | } 152 | ], sortColumn('col1')) 153 | ).toEqual([ 154 | { 155 | name: 'col1', 156 | sort: 'desc' 157 | } 158 | ]); 159 | }); 160 | 161 | it('sorts column from desc', () => { 162 | expect( 163 | reducer([ 164 | { 165 | name: 'col1', 166 | sort: 'desc' 167 | } 168 | ], sortColumn('col1')) 169 | ).toEqual([ 170 | { 171 | name: 'col1', 172 | sort: null 173 | } 174 | ]); 175 | }); 176 | 177 | it('removes sort from another columns', () => { 178 | expect( 179 | reducer([ 180 | { 181 | name: 'col1', 182 | sort: 'desc' 183 | }, 184 | { 185 | name: 'col2' 186 | }, 187 | { 188 | name: 'col3' 189 | } 190 | ], sortColumn('col2')) 191 | ).toEqual([ 192 | { 193 | name: 'col1', 194 | sort: null 195 | }, 196 | { 197 | name: 'col2', 198 | sort: 'asc' 199 | }, 200 | { 201 | name: 'col3' 202 | } 203 | ]); 204 | }); 205 | }); 206 | -------------------------------------------------------------------------------- /src/reducers/gridState.js: -------------------------------------------------------------------------------- 1 | export default (state = {}, action) => { 2 | switch (action.type) { 3 | case 'MARK_MOVE_DEST': 4 | return Object.assign({}, state, { 5 | moving: { 6 | name: action.name, 7 | left: action.left, 8 | right: action.right 9 | } 10 | }); 11 | 12 | case 'MOVE_COLUMN': 13 | return Object.assign({}, state, { 14 | moving: null 15 | }); 16 | 17 | case 'RESIZE_COLUMN': 18 | return Object.assign({}, state, { 19 | resizing: null 20 | }); 21 | 22 | case 'SELECT_ROW': 23 | return Object.assign({}, state, { 24 | selectedIndex: action.rowIndex 25 | }); 26 | 27 | case 'FILTER_COLUMN': 28 | case 'SORT_COLUMN': 29 | return Object.assign({}, state, { 30 | pager: state.pager ? Object.assign({}, state.pager, { page: 0 }) : null 31 | }); 32 | 33 | default: 34 | return state; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/reducers/gridState.test.js: -------------------------------------------------------------------------------- 1 | import gridStateReducer from './gridState.js'; 2 | import { markMoveDest, moveColumn, resizeColumn, filterColumn, sortColumn, selectRow } from '../actionCreators.js'; 3 | 4 | describe('gridState reducer', () => { 5 | it('marks column move destination', () => { 6 | expect( 7 | gridStateReducer({ 8 | userParam: -1 9 | }, markMoveDest('col1', 'col2', 'col3')) 10 | ).toEqual({ 11 | userParam: -1, 12 | moving: { 13 | name: 'col1', 14 | left: 'col2', 15 | right: 'col3' 16 | } 17 | }); 18 | }); 19 | 20 | it('clears marks on column move', () => { 21 | expect( 22 | gridStateReducer({ 23 | userParam: -1, 24 | moving: { 25 | name: 'col1', 26 | left: 'col2', 27 | right: 'col3' 28 | } 29 | }, moveColumn('col1', 'col2', 'col3')) 30 | ).toEqual({ 31 | userParam: -1, 32 | moving: null 33 | }); 34 | }); 35 | 36 | it('clears ghost mark on column resize', () => { 37 | expect( 38 | gridStateReducer({ 39 | userParam: -1, 40 | resizing: { 41 | name: 'col', 42 | position: 20 43 | } 44 | }, resizeColumn('col', 10)) 45 | ).toEqual({ 46 | userParam: -1, 47 | resizing: null 48 | }); 49 | }); 50 | 51 | it('selects row', () => { 52 | expect( 53 | gridStateReducer({ 54 | selectedIndex: 1, 55 | param: -1 56 | }, selectRow(3)) 57 | ).toEqual({ 58 | selectedIndex: 3, 59 | param: -1 60 | }); 61 | }); 62 | 63 | it('refreshes page on filter', () => { 64 | expect( 65 | gridStateReducer({ 66 | userParam1: -1, 67 | pager: { 68 | userParam2: -1, 69 | page: 10, 70 | size: 10 71 | } 72 | }, filterColumn('any', 'column')) 73 | ).toEqual({ 74 | userParam1: -1, 75 | pager: { 76 | userParam2: -1, 77 | page: 0, 78 | size: 10 79 | } 80 | }); 81 | expect( 82 | gridStateReducer({ 83 | userParam1: -1, 84 | pager: null 85 | }, filterColumn('any', 'column')) 86 | ).toEqual({ 87 | userParam1: -1, 88 | pager: null 89 | }); 90 | }); 91 | 92 | it('refreshes page on sort', () => { 93 | expect( 94 | gridStateReducer({ 95 | userParam1: -1, 96 | pager: { 97 | userParam2: -1, 98 | page: 10, 99 | size: 10 100 | } 101 | }, sortColumn('any')) 102 | ).toEqual({ 103 | userParam1: -1, 104 | pager: { 105 | userParam2: -1, 106 | page: 0, 107 | size: 10 108 | } 109 | }); 110 | expect( 111 | gridStateReducer({ 112 | userParam1: -1, 113 | pager: null 114 | }, sortColumn('any')) 115 | ).toEqual({ 116 | userParam1: -1, 117 | pager: null 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import columnsReducer from './columns.js'; 2 | import gridStateReducer from './gridState.js'; 3 | 4 | export default (state = {}, action) => Object.assign({}, state, { 5 | columns: columnsReducer(state.columns, action), 6 | gridState: gridStateReducer(state.gridState, action) 7 | }); 8 | -------------------------------------------------------------------------------- /src/reducers/index.test.js: -------------------------------------------------------------------------------- 1 | import reducer from './index.js'; 2 | 3 | const randomAction = { 4 | type: '__RANDOM_ACTION__' 5 | }; 6 | 7 | describe('reducer', () => { 8 | it('inits', () => { 9 | expect( 10 | reducer(undefined, randomAction) 11 | ).toEqual({ 12 | columns: [], 13 | gridState: {} 14 | }); 15 | }); 16 | 17 | it('saves previous user state', () => { 18 | expect( 19 | reducer({ 20 | userState: { 21 | param: 1 22 | } 23 | }, randomAction) 24 | ).toEqual({ 25 | userState: { 26 | param: 1 27 | }, 28 | columns: [], 29 | gridState: {} 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/utils/bisectColumns.js: -------------------------------------------------------------------------------- 1 | import findColumn from './findColumn.js'; 2 | 3 | export default (columns, position) => { 4 | if (columns.length < 2) { 5 | return [-1, 0]; 6 | } 7 | const [columnIndex, columnPosition] = findColumn(columns, position); 8 | if (columnIndex === -1) { 9 | return [-1, 0]; 10 | } 11 | if (columnIndex === columns.length) { 12 | return [columns.length - 1, -1]; 13 | } 14 | const width = columns[columnIndex].width; 15 | if (columnPosition < width / 2) { 16 | return [columnIndex - 1, columnIndex]; 17 | } else if (columnIndex === columns.length - 1) { 18 | return [columnIndex, -1]; 19 | } else { 20 | return [columnIndex, columnIndex + 1]; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/bisectColumns.test.js: -------------------------------------------------------------------------------- 1 | import { bisectColumns } from './index.js'; 2 | 3 | describe('bisectColumns', () => { 4 | it('selects first if no one column is present', () => { 5 | expect(bisectColumns([], 200)).toEqual([-1, 0]); 6 | }); 7 | 8 | it('selects first if only one column is present', () => { 9 | expect(bisectColumns([ 10 | { width: 60 } 11 | ], 200)).toEqual([-1, 0]); 12 | }); 13 | 14 | it('selects first column if position is negative', () => { 15 | expect(bisectColumns([ 16 | { width: 60 }, 17 | { width: 60 } 18 | ], -20)).toEqual([-1, 0]); 19 | }); 20 | 21 | it('selects last column if position is in the right half of it', () => { 22 | expect(bisectColumns([ 23 | { width: 60 }, 24 | { width: 60 } 25 | ], 100)).toEqual([1, -1]); 26 | }); 27 | 28 | it('selects last column if position is greater than all columns width', () => { 29 | expect(bisectColumns([ 30 | { width: 60 }, 31 | { width: 60 } 32 | ], 200)).toEqual([1, -1]); 33 | }); 34 | 35 | it('selects previous and current if position in column is less than half', () => { 36 | expect(bisectColumns([ 37 | { width: 60 }, 38 | { width: 60 }, 39 | { width: 60 } 40 | ], 89)).toEqual([0, 1]); 41 | }); 42 | 43 | it('selects current and next if position in column is greater or equal than half', () => { 44 | expect(bisectColumns([ 45 | { width: 60 }, 46 | { width: 60 }, 47 | { width: 60 } 48 | ], 90)).toEqual([1, 2]); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/utils/checkProps.js: -------------------------------------------------------------------------------- 1 | export default (...list) => (props, nextProps) => 2 | list.some(key => props[key] !== nextProps[key]); 3 | -------------------------------------------------------------------------------- /src/utils/checkProps.test.js: -------------------------------------------------------------------------------- 1 | import checkProps from './checkProps.js'; 2 | 3 | describe('checkProps util', () => { 4 | it('takes a list of props names and returns a function comparator', () => { 5 | const compare = checkProps('param1', 'param2'); 6 | expect(typeof compare).toEqual('function'); 7 | }); 8 | 9 | it('compares props presented in a list', () => { 10 | const compare = checkProps('param1', 'param2'); 11 | expect(compare({ param1: 1, param2: 3 }, { param1: 1, param2: 3 })).toEqual(false); 12 | expect(compare({ param1: 1, param2: 3 }, { param1: 2, param2: 3 })).toEqual(true); 13 | expect(compare({ param1: 1, param2: 3 }, { param1: 2, param2: 4 })).toEqual(true); 14 | }); 15 | 16 | it('does not compare props not presented in a list', () => { 17 | const compare = checkProps('param1'); 18 | expect(compare({ param2: 1 }, { param2: 1 })).toEqual(false); 19 | expect(compare({ param2: 1 }, { param2: 2 })).toEqual(false); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/utils/compose.js: -------------------------------------------------------------------------------- 1 | export default function compose(...funcs) { 2 | if (funcs.length === 0) { 3 | return arg => arg; 4 | } 5 | 6 | if (funcs.length === 1) { 7 | return funcs[0]; 8 | } 9 | 10 | return funcs.reduce((a, b) => (...args) => a(b(...args))); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/compose.test.js: -------------------------------------------------------------------------------- 1 | import compose from './compose.js'; 2 | 3 | describe('compose', () => { 4 | it('composes from right to left', () => { 5 | const double = x => x * 2; 6 | const square = x => x * x; 7 | expect(compose(square)(5)).toEqual(25); 8 | expect(compose(square, double)(5)).toEqual(100); 9 | expect(compose(double, square, double)(5)).toEqual(200); 10 | }); 11 | 12 | it('can be seeded with multiple arguments', () => { 13 | const square = x => x * x; 14 | const add = (x, y) => x + y; 15 | expect(compose(square, add)(1, 2)).toEqual(9); 16 | }); 17 | 18 | it('returns the identity function if given no arguments', () => { 19 | expect(compose()(1, 2)).toEqual(1); 20 | expect(compose()(3)).toEqual(3); 21 | expect(compose()()).toEqual(undefined); 22 | }); 23 | 24 | it('returns the first function if given only one', () => { 25 | const fn = () => {}; 26 | expect(compose(fn)).toEqual(fn); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/utils/findColumn.js: -------------------------------------------------------------------------------- 1 | export default (columns, x) => { 2 | if (x < 0 || columns.length === 0) { 3 | return [-1, x]; 4 | } 5 | let sum = 0; 6 | return columns.reduce((acc, { width }, index) => { 7 | const left = sum; 8 | sum += width; 9 | if (left <= x && x < sum) { 10 | return [index, x - left]; 11 | } 12 | if (sum === x) { 13 | return [index, width]; 14 | } 15 | if (sum < x) { 16 | return [index + 1, x - sum]; 17 | } 18 | return acc; 19 | }, null); 20 | }; 21 | -------------------------------------------------------------------------------- /src/utils/findColumn.test.js: -------------------------------------------------------------------------------- 1 | import findColumn from './findColumn.js'; 2 | 3 | describe('findColumn util', () => { 4 | it('finds column by width property', () => { 5 | expect( 6 | findColumn([{ width: 25 }, { width: 25 }, { width: 25 } ], 40) 7 | ).toEqual([1, 15]); 8 | }); 9 | 10 | it('finds column in the edge of the first one', () => { 11 | expect( 12 | findColumn([{ width: 25 }], 0) 13 | ).toEqual([0, 0]); 14 | }); 15 | 16 | it('returns -1 for preseding x', () => { 17 | expect( 18 | findColumn([{ width: 25 }], -5) 19 | ).toEqual([-1, -5]); 20 | }); 21 | 22 | it('finds column in the edge of two ones', () => { 23 | expect( 24 | findColumn([{ width: 25 }, { width: 25 }], 25) 25 | ).toEqual([1, 0]); 26 | }); 27 | 28 | it('finds column in the edge of the last one', () => { 29 | expect( 30 | findColumn([{ width: 25 }], 25) 31 | ).toEqual([0, 25]); 32 | }); 33 | 34 | it('returns count of columns for following x', () => { 35 | expect( 36 | findColumn([{ width: 25 }], 30) 37 | ).toEqual([1, 5]); 38 | }); 39 | 40 | it('returns -1 when there is no one column ', () => { 41 | expect( 42 | findColumn([], -10) 43 | ).toEqual([-1, -10]); 44 | expect( 45 | findColumn([], 10) 46 | ).toEqual([-1, 10]); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/utils/getKeysByIndex.js: -------------------------------------------------------------------------------- 1 | const empty = () => Object.create(null); 2 | 3 | const invert = obj => Object.keys(obj).reduce((acc, key) => (acc[obj[key]] = key, acc), empty()); 4 | 5 | const createFreeKeyGenerator = (indexes) => { 6 | let lastPointer = 0; 7 | return () => { 8 | while (true) { 9 | const key = `key_${lastPointer}`; 10 | lastPointer += 1; 11 | if (!(key in indexes)) { 12 | return key; 13 | } 14 | } 15 | }; 16 | }; 17 | 18 | const getRecoveredKeys = (lastKeys, start, end) => { 19 | const keys = empty(); 20 | for (let i = start; i < end + 1; i += 1) { 21 | if (i in lastKeys) { 22 | keys[i] = lastKeys[i]; 23 | } 24 | } 25 | return keys; 26 | }; 27 | 28 | const generateKeys = (lastKeys, start, end) => { 29 | const keys = empty(); 30 | const generateKey = createFreeKeyGenerator(invert(lastKeys)); 31 | for (let i = start; i < end + 1; i += 1) { 32 | if (!(i in lastKeys)) { 33 | keys[i] = generateKey(); 34 | } 35 | } 36 | return keys; 37 | }; 38 | 39 | export default (lastKeys = empty(), start, end) => { 40 | const recovered = getRecoveredKeys(lastKeys, start, end); 41 | const generated = generateKeys(recovered, start, end); 42 | return Object.assign(empty(), recovered, generated); 43 | }; 44 | -------------------------------------------------------------------------------- /src/utils/getKeysByIndex.test.js: -------------------------------------------------------------------------------- 1 | import { getKeysByIndex } from './index.js'; 2 | 3 | describe('getKeysByIndex', () => { 4 | it('should generate new keys', () => { 5 | expect(getKeysByIndex(undefined, 0, 2)).toEqual({ 6 | 0: 'key_0', 7 | 1: 'key_1', 8 | 2: 'key_2' 9 | }); 10 | }); 11 | 12 | it('should reuse passed keys', () => { 13 | expect(getKeysByIndex({ 14 | 2: 'key_0', 15 | 3: 'key_1' 16 | }, 0, 3)).toEqual({ 17 | 0: 'key_2', 18 | 1: 'key_3', 19 | 2: 'key_0', 20 | 3: 'key_1' 21 | }); 22 | }); 23 | 24 | it('should ignore pass keys with expired indexes', () => { 25 | expect(getKeysByIndex({ 26 | 0: 'key_0', 27 | 1: 'key_1', 28 | 2: 'key_2', 29 | 3: 'key_3' 30 | }, 2, 5)).toEqual({ 31 | 2: 'key_2', 32 | 3: 'key_3', 33 | 4: 'key_0', 34 | 5: 'key_1' 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/utils/getVisibleRows.js: -------------------------------------------------------------------------------- 1 | export default ({ scrollTop, viewportHeight, rowHeight, rowsCount }) => { 2 | if (viewportHeight < 0) { 3 | return [0, 0]; 4 | } 5 | const clusterSize = Math.ceil(viewportHeight / rowHeight); 6 | const topCluster = Math.floor(Math.floor(Math.max(0, scrollTop) / rowHeight) / clusterSize); 7 | const end = Math.min(rowsCount, (topCluster + 2) * clusterSize); 8 | // count of visible rows is always the same then focus never be lost 9 | const start = Math.max(0, end - clusterSize * 2); 10 | return [start, end]; 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/getVisibleRows.test.js: -------------------------------------------------------------------------------- 1 | import { getVisibleRows } from './index.js'; 2 | 3 | describe('getVisibleRows', () => { 4 | it('at the start', () => { 5 | expect( 6 | getVisibleRows({ 7 | viewportHeight: 100, 8 | rowHeight: 12, 9 | rowsCount: 60, 10 | scrollTop: 0 11 | }) 12 | ).toEqual([0, 18]); 13 | }); 14 | 15 | it('between the first and the second sectors', () => { 16 | expect( 17 | getVisibleRows({ 18 | viewportHeight: 100, 19 | rowHeight: 12, 20 | rowsCount: 60, 21 | scrollTop: 9 * 12 - 1 22 | }) 23 | ).toEqual([0, 18]); 24 | expect( 25 | getVisibleRows({ 26 | viewportHeight: 100, 27 | rowHeight: 12, 28 | rowsCount: 60, 29 | scrollTop: 9 * 12 30 | }) 31 | ).toEqual([9, 27]); 32 | }); 33 | 34 | it('between the second and the third sectors', () => { 35 | expect( 36 | getVisibleRows({ 37 | viewportHeight: 100, 38 | rowHeight: 12, 39 | rowsCount: 60, 40 | scrollTop: 18 * 12 - 1 41 | }) 42 | ).toEqual([9, 27]); 43 | expect( 44 | getVisibleRows({ 45 | viewportHeight: 100, 46 | rowHeight: 12, 47 | rowsCount: 60, 48 | scrollTop: 18 * 12 49 | }) 50 | ).toEqual([18, 36]); 51 | }); 52 | 53 | it('should save count of visible rows the same to not lost focus', () => { 54 | expect( 55 | getVisibleRows({ 56 | viewportHeight: 100, 57 | rowHeight: 12, 58 | rowsCount: 54, 59 | scrollTop: 52 * 12 - 100 60 | }) 61 | ).toEqual([36, 54]); 62 | expect( 63 | getVisibleRows({ 64 | viewportHeight: 100, 65 | rowHeight: 12, 66 | rowsCount: 60, 67 | scrollTop: 60 * 12 - 100 68 | }) 69 | ).toEqual([42, 60]); 70 | }); 71 | 72 | it('should save count of visible rows with negative scrollTop', () => { 73 | expect( 74 | getVisibleRows({ 75 | viewportHeight: 100, 76 | rowHeight: 12, 77 | rowsCount: 60, 78 | scrollTop: -20 79 | }) 80 | ).toEqual([0, 18]); 81 | }); 82 | 83 | it('should trim negative header height', () => { 84 | expect( 85 | getVisibleRows({ 86 | viewportHeight: -100, 87 | rowHeight: 12, 88 | rowsCount: 10, 89 | scrollTop: 0 90 | }) 91 | ).toEqual([0, 0]); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as compose } from './compose.js'; 2 | export { default as checkProps } from './checkProps.js'; 3 | export { default as getKeysByIndex } from './getKeysByIndex.js'; 4 | export { default as getVisibleRows } from './getVisibleRows.js'; 5 | export { default as bisectColumns } from './bisectColumns.js'; 6 | export { default as findColumn } from './findColumn.js'; 7 | export { default as trimColumnWidth } from './trimColumnWidth.js'; 8 | export { default as transform } from './transform.js'; 9 | -------------------------------------------------------------------------------- /src/utils/testUtils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | export const mount = (MountingComponent) => { 5 | const wrapper = document.createElement('div'); 6 | return { 7 | getWrapper: () => wrapper, 8 | setProps: props => ReactDOM.render(, wrapper) 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/transform.js: -------------------------------------------------------------------------------- 1 | const filterRowByColumns = (row, columns) => 2 | columns.every(column => String(row[column.name]).toLowerCase().indexOf(column.filter) !== -1); 3 | 4 | const sortRowsByColumns = (a, b, column) => { 5 | if (a[column.name] === b[column.name]) { 6 | return 0; 7 | } 8 | if (column.sort === 'asc') { 9 | return a[column.name] < b[column.name] ? -1 : 1; 10 | } 11 | return a[column.name] > b[column.name] ? -1 : 1; 12 | }; 13 | 14 | export default (state, data) => { 15 | const filteredColumns = state.columns.filter(column => column.filter); 16 | const sortedColumn = state.columns.find(column => column.sort === 'asc' || column.sort === 'desc'); 17 | const filtered 18 | = filteredColumns.length 19 | ? data.filter(datum => filterRowByColumns(datum, filteredColumns)) 20 | : data; 21 | const sorted 22 | = sortedColumn 23 | ? filtered.slice().sort((a, b) => sortRowsByColumns(a, b, sortedColumn)) 24 | : filtered; 25 | const pager = state.gridState && state.gridState.pager; 26 | const sliced 27 | = pager 28 | ? sorted.slice(pager.page * pager.size, pager.page * pager.size + pager.size) 29 | : sorted; 30 | return sliced; 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/transform.test.js: -------------------------------------------------------------------------------- 1 | import transform from './transform.js'; 2 | 3 | describe('transform', () => { 4 | it('filters by column', () => { 5 | expect( 6 | transform({ 7 | columns: [ 8 | { 9 | name: 'col', 10 | filter: 'tit' 11 | } 12 | ] 13 | }, [ 14 | { 15 | id: 1, 16 | col: 'Title' 17 | }, 18 | { 19 | id: 2, 20 | col: 'lorem' 21 | }, 22 | { 23 | id: 3, 24 | col: 'title' 25 | }, 26 | { 27 | id: 4, 28 | col: 'pretit' 29 | }, 30 | { 31 | id: 5, 32 | col: 'lorem' 33 | } 34 | ]) 35 | ).toEqual([ 36 | { 37 | id: 1, 38 | col: 'Title' 39 | }, 40 | { 41 | id: 3, 42 | col: 'title' 43 | }, 44 | { 45 | id: 4, 46 | col: 'pretit' 47 | } 48 | ]); 49 | }); 50 | 51 | it('filters by all columns', () => { 52 | expect( 53 | transform({ 54 | columns: [ 55 | { 56 | name: 'col1', 57 | filter: 'tit' 58 | }, 59 | { 60 | name: 'col2', 61 | filter: 'val' 62 | } 63 | ] 64 | }, [ 65 | { 66 | id: 1, 67 | col1: 'tit', 68 | col2: 'lorem' 69 | }, 70 | { 71 | id: 2, 72 | col1: 'lorem', 73 | col2: 'val' 74 | }, 75 | { 76 | id: 3, 77 | col1: 'pretitle', 78 | col2: 'prevalue' 79 | } 80 | ]) 81 | ).toEqual([ 82 | { 83 | id: 3, 84 | col1: 'pretitle', 85 | col2: 'prevalue' 86 | } 87 | ]); 88 | }); 89 | 90 | it('sorts by column asc', () => { 91 | expect( 92 | transform({ 93 | columns: [ 94 | { 95 | name: 'col', 96 | sort: 'asc' 97 | } 98 | ] 99 | }, [ 100 | { 101 | col: 2 102 | }, 103 | { 104 | col: 1 105 | }, 106 | { 107 | col: 1 108 | }, 109 | { 110 | col: 3 111 | } 112 | ]) 113 | ).toEqual([ 114 | { 115 | col: 1 116 | }, 117 | { 118 | col: 1 119 | }, 120 | { 121 | col: 2 122 | }, 123 | { 124 | col: 3 125 | } 126 | ]); 127 | }); 128 | 129 | it('sorts by column desc', () => { 130 | expect( 131 | transform({ 132 | columns: [ 133 | { 134 | name: 'col', 135 | sort: 'desc' 136 | } 137 | ] 138 | }, [ 139 | { 140 | col: 2 141 | }, 142 | { 143 | col: 1 144 | }, 145 | { 146 | col: 3 147 | } 148 | ]) 149 | ).toEqual([ 150 | { 151 | col: 3 152 | }, 153 | { 154 | col: 2 155 | }, 156 | { 157 | col: 1 158 | } 159 | ]); 160 | }); 161 | 162 | it('filters and sorts by column', () => { 163 | expect( 164 | transform({ 165 | columns: [ 166 | { 167 | name: 'col', 168 | sort: 'desc', 169 | filter: 'tit' 170 | } 171 | ] 172 | }, [ 173 | { 174 | col: 'tit3' 175 | }, 176 | { 177 | col: 'lorem1' 178 | }, 179 | { 180 | col: 'title2' 181 | } 182 | ]) 183 | ).toEqual([ 184 | { 185 | col: 'title2' 186 | }, 187 | { 188 | col: 'tit3' 189 | }, 190 | ]); 191 | }); 192 | 193 | it('slices page', () => { 194 | expect( 195 | transform({ 196 | columns: [], 197 | gridState: { 198 | pager: { 199 | page: 1, 200 | size: 3 201 | } 202 | } 203 | }, [ 204 | { col: 'row1' }, 205 | { col: 'row2' }, 206 | { col: 'row3' }, 207 | { col: 'row4' }, 208 | { col: 'row5' }, 209 | { col: 'row6' }, 210 | { col: 'row7' }, 211 | { col: 'row8' }, 212 | { col: 'row9' } 213 | ]) 214 | ).toEqual([ 215 | { col: 'row4' }, 216 | { col: 'row5' }, 217 | { col: 'row6' }, 218 | ]); 219 | }); 220 | 221 | it('filters numbers', () => { 222 | expect( 223 | transform({ 224 | columns: [ 225 | { 226 | name: 'col', 227 | filter: '123', 228 | sort: 'desc' 229 | } 230 | ] 231 | }, [ 232 | { 233 | col: 1234 234 | }, 235 | { 236 | col: 4123 237 | }, 238 | { 239 | col: 5567 240 | } 241 | ]) 242 | ).toEqual([ 243 | { 244 | col: 4123 245 | }, 246 | { 247 | col: 1234 248 | } 249 | ]); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /src/utils/trimColumnWidth.js: -------------------------------------------------------------------------------- 1 | export default (column, width) => Math.min(Math.max(column.minWidth || 0, width), column.maxWidth || Infinity); 2 | -------------------------------------------------------------------------------- /src/utils/trimColumnWidth.test.js: -------------------------------------------------------------------------------- 1 | import { trimColumnWidth } from './index.js'; 2 | 3 | describe('trimColumnWidth', () => { 4 | it('trims value with minWidth', () => { 5 | expect(trimColumnWidth({ minWidth: 100 }, 90)).toEqual(100); 6 | }); 7 | 8 | it('trims value with maxWidth', () => { 9 | expect(trimColumnWidth({ maxWidth: 200 }, 210)).toEqual(200); 10 | }); 11 | 12 | it('trims with default minWidth 0', () => { 13 | expect(trimColumnWidth({}, -10)).toEqual(0); 14 | }); 15 | 16 | it('does not trim in range', () => { 17 | expect(trimColumnWidth({}, 500)).toEqual(500); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const BabiliWebpackPlugin = require('babili-webpack-plugin'); 4 | 5 | const production = process.env.NODE_ENV === 'production'; 6 | 7 | module.exports = { 8 | entry: './src/examples/index.js', 9 | output: { 10 | path: __dirname, 11 | filename: 'examples.js' 12 | }, 13 | devtool: !production && '#inline-source-map', 14 | resolve: { 15 | mainFields: [ 16 | 'inferno:main', 17 | 'browser', 18 | 'module', 19 | 'main' 20 | ], 21 | }, 22 | module: { 23 | strictExportPresence: true, 24 | rules: [ 25 | { 26 | test: /\.css$/, 27 | use: [ 28 | 'style-loader', 29 | { 30 | loader: 'css-loader', 31 | options: { 32 | minimize: production 33 | } 34 | } 35 | ] 36 | }, 37 | { 38 | test: /\.js$/, 39 | exclude: /node_modules/, 40 | use: [ 41 | 'babel-loader', 42 | 'eslint-loader' 43 | ] 44 | } 45 | ] 46 | }, 47 | plugins: [ 48 | new HtmlWebpackPlugin() 49 | ] 50 | }; 51 | 52 | if (production) { 53 | module.exports.plugins.push( 54 | new webpack.DefinePlugin({ 55 | 'process.env.NODE_ENV': JSON.stringify('production'), 56 | 'process.env.DEBUG': true 57 | }), 58 | new BabiliWebpackPlugin() 59 | ); 60 | } 61 | --------------------------------------------------------------------------------