├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── doc ├── 50000rows_200cols.gif ├── api.gif ├── cell-style-customization.png ├── clipboard.png ├── column-width-resize.png ├── custom-color-picker.gif ├── custom-combobox.gif ├── frozen-rows-cols.png ├── initial-sort.png ├── material-select-multi.gif ├── quickstart.png ├── react-ws-canvas-layout.dxf ├── selection-mode-cells.png ├── selection-mode-multi.png ├── selection-mode-rows.png ├── text-wrap.gif └── text-wrap.png ├── example ├── .env ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── sandbox.config.json ├── src │ ├── App.quickstart.tsx │ ├── Frame.tsx │ ├── Sample1.tsx │ ├── Sample2.tsx │ ├── Sample3.tsx │ ├── Sample4.tsx │ ├── Sample5.tsx │ ├── Sample6.tsx │ ├── index.css │ ├── index.tsx │ ├── lib │ │ ├── Utils.tsx │ │ ├── WSCanvas.tsx │ │ ├── WSCanvasApi.tsx │ │ ├── WSCanvasCellCoord.tsx │ │ ├── WSCanvasColumn.tsx │ │ ├── WSCanvasCoord.tsx │ │ ├── WSCanvasEditMode.tsx │ │ ├── WSCanvasFilter.tsx │ │ ├── WSCanvasProps.tsx │ │ ├── WSCanvasPropsDefault.tsx │ │ ├── WSCanvasRect.tsx │ │ ├── WSCanvasScrollbarMode.tsx │ │ ├── WSCanvasSelection.tsx │ │ ├── WSCanvasSelectionBounds.tsx │ │ ├── WSCanvasSelectionMode.tsx │ │ ├── WSCanvasSelectionRange.tsx │ │ ├── WSCanvasSortDirection.tsx │ │ ├── WSCanvasState.tsx │ │ ├── WSCanvasStates.tsx │ │ ├── WSCanvasXYCellCoord.tsx │ │ └── index.tsx │ ├── quickstart │ │ └── index.tsx │ ├── react-app-env.d.ts │ └── serviceWorker.ts └── tsconfig.json ├── lib ├── local-publish ├── minor-and-publish ├── package.json ├── prepatch-and-publish ├── rollup.config.js └── tsconfig.json ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | node_modules 26 | dist 27 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Lorenzo Delana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react ws canvas 2 | 3 | [![NPM](https://img.shields.io/npm/v/react-ws-canvas.svg)](https://www.npmjs.com/package/react-ws-canvas) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 4 | 5 | Spreadsheet like react canvas datagrid optimized for performance built entirely typescript and react functional components with react hooks. 6 | 7 | [**LIVE DEMO**](https://codesandbox.io/s/github/devel0/react-ws-canvas/tree/master/example) 8 | 9 | --- 10 | 11 | - [recent changes](#recent-changes) 12 | - [features](#features) 13 | - [todo](#todo) 14 | - [quickstart](#quickstart) 15 | - [examples list](#examples-list) 16 | - [tips](#tips) 17 | - [how to contribute ( quickstart )](#how-to-contribute--quickstart-) 18 | - [local deploy](#local-deploy) 19 | - [how this project was built](#how-this-project-was-built) 20 | - [development notes](#development-notes) 21 | 22 | --- 23 | 24 | ## recent changes 25 | 26 | - v0.23.6 27 | - fix custom render component visibiliy when column scrolls 28 | - v0.23.5 29 | - infinite rearrangement workaround ; [repro](https://codesandbox.io/s/github/devel0/react-ws-canvas/tree/infinite_rearrange_repro/example) start sample5 and size height of the window like the grid then add items until last in the grid will start infinite rearrange 30 | - v0.23.4 31 | - added margin when wrap custom rendere objects 32 | 33 | ## issues and improvements roadmap 34 | 35 | - can't deselect cell ( a better negative selection should implement with a class that implements a contains operator that matches in order selection pattern, for example select all then deselect a cell ) 36 | - smooth scroll ( actually scroll by row and col and not partial by pixel ) 37 | - a complete refactoring with better state management ( collect all feature currently available and rewrite the code with better modularization and functional correspondance ) 38 | 39 | ## features 40 | 41 | - **canvas based** high performance datagrid ( able to scroll with ease millions of rows maintaining immediate cell focus and editing features ) 42 | 43 | 50000 rows x 200 cols example ( [LIVE EXAMPLE](https://codesandbox.io/s/github/devel0/react-ws-canvas/tree/master/example) : click *EX1* button ) 44 | 45 | ![](doc/50000rows_200cols.gif) 46 | 47 | - **direct cell editing**, just click on a cell then type, hit ENTER or arrows keys to move next ( [native cell types][1] ) 48 | - "text": type text to change cell ; CANC to clear cell ; CTRL+C / CTRL+V to copy/paste 49 | - "boolean": toggle boolean with keyboard space when cell focused 50 | - "date", "time", "datetime": smart date insertion ( typing 121030 results in 12/10/2030 ) browser locale supported 51 | - "number": sci numbers ( typing 12e-3 results in 0.012 displayed ) browser locale support for decimal separators 52 | 53 | - [properties default values][22] 54 | 55 | - [cell or row][2] selection mode 56 | 57 | ![](doc/selection-mode-cells.png) 58 | 59 | ![](doc/selection-mode-rows.png) 60 | 61 | - selection [mode multi][9] 62 | 63 | ![](doc/selection-mode-multi.png) 64 | 65 | - frozen [rows, cols][3] 66 | 67 | ![](doc/frozen-rows-cols.png) 68 | 69 | - [wrap][21] text cells ( [helper][121] ) 70 | 71 | ![](doc/text-wrap.gif) 72 | 73 | - rows and cols numbering can be [shown or hidden][5] 74 | 75 | - if column numbering visible automatic sort can be customized through [less-than-op][6] ( [helper][106] ) 76 | 77 | - [column click behavior][12] can be full column select, column toggle sort or none to disable sort/select behavior 78 | 79 | - [column header][15] can be customized ( [helper][115] ) 80 | 81 | - canvas size can be specified through width, height ( [fullwidth][26] option available ) 82 | 83 | - column width can be changed intractively using mouse 84 | 85 | ![](doc/column-width-resize.png) 86 | 87 | - column width [autoexpand][16] 88 | 89 | - column custom [initial sort][18] ( *note*: prepareCellDataset, rowSetCellData, commitCellDataset must defined to make sort working ); also see [example6][36] for tip about ensure initial sort on subsequent datasource applications 90 | 91 | ![](doc/initial-sort.png) 92 | 93 | - data getter/setter can follow a [worksheet][7] or a [db record type][8] ( [example of nested field][28] using [getFieldData][29] and [setFieldData][30] methods) 94 | 95 | - data [filter global][129] 96 | 97 | - optional dataset on external object with [ds rows getter mapper][32] useful in some circumstance when need to preserve rows array object ref 98 | 99 | - [api][10] and [handlers][27] available for control interactions ( [example][11] ) ; props can be accessed [inversely][31] through api; retrieve list of selected row idxs [example][38] 100 | 101 | ![](doc/api.gif) 102 | 103 | - each individual cell [custom edit][13] ( F2 ) control can be customized also in [column helper][113] ( [example][24] : through keyboard F2, arrows then enter ) ; cell [editing/edited][128] also in [column helper][127] 104 | 105 | ![](doc/custom-combobox.gif) 106 | 107 | - custom multi select with material-ui ( [example][35] ) 108 | 109 | - custom date picker with material pickers ( [example][37] ) 110 | 111 | ![](doc/material-select-multi.gif) 112 | 113 | - custom color picker with **custom render** ( [example][130] ) ; note: wrapText will use custom component height to *autoresize row height* ; *filter* can work as text mode defining renderTransform 114 | 115 | ![](doc/custom-color-picker.gif) 116 | 117 | - cell [background][19], [font and color][119], [readonly mode][126] customization and [text align][25] also with [helper][125] 118 | 119 | ![](doc/cell-style-customization.png) 120 | 121 | - container [height min][23] and canvas [styles][20] 122 | 123 | - each individual [cell type][14] can be customized ( [column helper][114] ) 124 | 125 | - clipboard copy to/from spreadsheet with ctrl-c and ctrl-v or [api][34] ( [example api][33] ) 126 | 127 | ![](doc/clipboard.png) 128 | 129 | - support mobile touch scrolling rows, cols and scrollbars 130 | 131 | [1]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasColumn.tsx#L7 132 | [2]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L45 133 | [3]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L38-L41 134 | [5]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L50-L55 135 | [6]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L90-L91 136 | [106]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasColumn.tsx#L36-L37 137 | [7]: https://github.com/devel0/react-ws-canvas/blob/cbd9d6d75100a45cb7d0ee073c6d5ab03f3a354e/example/src/Sample1.tsx#L31-L35 138 | [8]: https://github.com/devel0/react-ws-canvas/blob/e893299287fd041f84e900262d5915bf8670fc6b/example/src/Sample4.tsx#L231-L240 139 | [9]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L42-L43 140 | [10]: https://github.com/devel0/react-ws-canvas/blob/53639eb02d4df298b4591c6f231c63cf4db703b2/example/src/lib/WSCanvasApi.tsx#L13 141 | [11]: https://github.com/devel0/react-ws-canvas/blob/e893299287fd041f84e900262d5915bf8670fc6b/example/src/Sample4.tsx#L150 142 | [12]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L58-L59 143 | [13]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L85-L87 144 | [113]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasColumn.tsx#L57-L58 145 | [14]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L92-L93 146 | [114]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasColumn.tsx#L22-L23 147 | [15]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L88-L89 148 | [115]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasColumn.tsx#L25-L26 149 | [16]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L34-L35 150 | [18]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L96-L97 151 | [118]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasColumn.tsx#L44-L48 152 | [19]:https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L101-L102 153 | [119]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L131-L140 154 | [20]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L181-L184 155 | [21]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L129-L130 156 | [121]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasColumn.tsx#L50-L51 157 | [22]: https://github.com/devel0/react-ws-canvas/blob/cbd9d6d75100a45cb7d0ee073c6d5ab03f3a354e/example/src/lib/WSCanvasPropsDefault.tsx#L11 158 | [23]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/Sample3.tsx#L222 159 | [24]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/Sample3.tsx#L90-L122 160 | [25]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L98-L99 161 | [125]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasColumn.tsx#L39 162 | [26]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L18-L19 163 | [27]: https://github.com/devel0/react-ws-canvas/blob/513632060951df8a20b83dee39667529cc0ac883/example/src/lib/WSCanvasProps.tsx#L191-L219 164 | [28]: https://github.com/devel0/react-ws-canvas/blob/d0767d8e1327ff69090ad0dbd7f4eefd907c7a69/example/src/Sample3.tsx#L169-L171 165 | [29]: https://github.com/devel0/react-ws-canvas/blob/cbd9d6d75100a45cb7d0ee073c6d5ab03f3a354e/example/src/Sample3.tsx#L229 166 | [30]: https://github.com/devel0/react-ws-canvas/blob/cbd9d6d75100a45cb7d0ee073c6d5ab03f3a354e/example/src/Sample3.tsx#L235 167 | [31]: https://github.com/devel0/react-ws-canvas/blob/e893299287fd041f84e900262d5915bf8670fc6b/example/src/Sample4.tsx#L201 168 | [32]: https://github.com/devel0/react-ws-canvas/blob/1606876a64be83e6c1c9e78cfdeeea5f243f52f5/example/src/lib/WSCanvasProps.tsx#L79-L80 169 | [33]: https://github.com/devel0/react-ws-canvas/blob/a6d7e61ebc2371f4817e4ba632ff653738d26c55/example/src/Sample3.tsx#L211 170 | [34]: https://github.com/devel0/react-ws-canvas/blob/a6d7e61ebc2371f4817e4ba632ff653738d26c55/example/src/lib/WSCanvasApi.tsx#L108-L115 171 | [35]: https://github.com/devel0/react-ws-canvas/blob/ba1c4483045065f12fa3c59f1c910ad34fdeb213/example/src/Sample5.tsx#L63-L116 172 | [36]: https://github.com/devel0/react-ws-canvas/blob/d115874c761a250f280ced9f3798ad531ac59075/example/src/Sample6.tsx#L26-L28 173 | [37]: https://github.com/devel0/react-ws-canvas/blob/58f253259b6dc563dadbcdd598a9f0b81d3baa26/example/src/Sample5.tsx#L122-L160 174 | [38]: https://github.com/devel0/react-ws-canvas/blob/315d2d22075c042897652253469272d8e4ee9171/example/src/Sample4.tsx#L289 175 | [126]: https://github.com/devel0/react-ws-canvas/blob/4c6ca74106654cec114887b542f64fb41a8cdd0f/example/src/Sample3.tsx#L262-L268 176 | [127]: https://github.com/devel0/react-ws-canvas/blob/b123792518147670b918a392a8894a98e5442ca5/example/src/Sample2.tsx#L52-L59 177 | [128]: https://github.com/devel0/react-ws-canvas/blob/b123792518147670b918a392a8894a98e5442ca5/example/src/Sample2.tsx#L151-L157 178 | [129]: https://github.com/devel0/react-ws-canvas/blob/b123792518147670b918a392a8894a98e5442ca5/example/src/Sample2.tsx#L126-L132 179 | [130]: https://github.com/devel0/react-ws-canvas/blob/2d8ec23eb40a059825d6ac4dc2520ad696468595/example/src/Sample5.tsx#L129-L132 180 | 181 | ## todo 182 | 183 | ## quickstart 184 | 185 | ![](doc/quickstart.png) 186 | 187 | - create react app 188 | 189 | eventually install create-react-app 190 | 191 | ```sh 192 | npm install -g create-react-app 193 | ``` 194 | 195 | ```sh 196 | create-react-app test --template typescript 197 | cd test 198 | yarn add react-ws-canvas 199 | ``` 200 | 201 | - edit `App.tsx` as follows 202 | 203 | ```ts 204 | import React, { useState, useEffect } from 'react'; 205 | import { WSCanvas, useWindowSize, WSCanvasColumnClickBehavior } from 'react-ws-canvas'; 206 | 207 | const AppQuickStart: React.FC = () => { 208 | const [rows, setRows] = useState([]); 209 | const winSize = useWindowSize(); 210 | 211 | const ROWS = 500000; 212 | const COLS = 20; 213 | 214 | useEffect(() => { 215 | 216 | const _rows = []; 217 | for (let ri = 0; ri < ROWS; ++ri) { 218 | const row = []; 219 | for (let ci = 0; ci < COLS; ++ci) { 220 | row.push("r:" + ri + " c:" + ci); 221 | } 222 | _rows.push(row); 223 | } 224 | 225 | setRows(_rows); 226 | }, []); 227 | 228 | return row[colIdx]} 235 | prepareCellDataset={() => rows.slice()} 236 | commitCellDataset={(q) => setRows(q)} 237 | rowSetCellData={(row, colIdx, value) => row[colIdx] = value} 238 | />; 239 | } 240 | 241 | export default AppQuickStart; 242 | ``` 243 | 244 | - run the app 245 | 246 | ```sh 247 | yarn start 248 | ``` 249 | 250 | ## examples list 251 | 252 | | example | description | 253 | |---|---| 254 | | [quickstart](example/src/App.quickstart.tsx) | 500000 x 20 grid with minimal props | 255 | | [Sample1](example/src/Sample1.tsx) | 50000 x 200 grid with frozen row/col, filter, custom column width | 256 | | [Sample2](example/src/Sample2.tsx) | 5000 x 6 grid db-record-like column mapping, initial sort, custom sort, api onMouseDown, global filter, cell changing/changed | 257 | | [Sample3](example/src/Sample3.tsx) | 5000 x 7 grid db-record-like, data interact del/change row, custom cell editor, rowHover | 258 | | [Sample4](example/src/Sample4.tsx) | add/insert/del/move/currentRealRowSel rows using api | 259 | | [Sample5](example/src/Sample5.tsx) | custom multi select with material-ui ; custom render chip with color picker | 260 | | [Sample6](example/src/Sample6.tsx) | resetView behavior to force sync ds ; resetSorting and resetFilters resetView arguments | 261 | 262 | ## tips 263 | 264 | **prevent direct editing on editor customized cells** 265 | 266 | customize onPreviewKeyDown event handler on datagrid and preventDefault for matching cell 267 | 268 | ```ts 269 | onPreviewKeyDown={(states, e) => { 270 | if (states.props.columns && api && api.isDirectEditingKey(e)) { 271 | const fieldname = states.props.columns[states.state.focusedCell.col].field; 272 | if (fieldname === "colname") { 273 | //const row = states.props.rows[states.state.focusedCell.row]; 274 | e.preventDefault(); 275 | } 276 | } 277 | }} 278 | ``` 279 | 280 | ## how to contribute ( quickstart ) 281 | 282 | - clone repo 283 | 284 | ```sh 285 | git clone https://github.com/devel0/react-ws-canvas.git 286 | ``` 287 | 288 | - open vscode 289 | 290 | ```sh 291 | cd react-ws-canvas 292 | code . 293 | ``` 294 | 295 | - from vscode, open terminal ctrl+\` and execute example application ( this allow you to set **breakpoints** directly on library source code from `lib` folder ) 296 | 297 | ```sh 298 | cd example 299 | yarn install 300 | yarn start 301 | ``` 302 | 303 | - start chrome session using F5 304 | 305 | ## local deploy 306 | 307 | - from library 308 | 309 | ```sh 310 | cd lib 311 | yarn build && yalc publish 312 | ``` 313 | 314 | - from your project 315 | 316 | ```sh 317 | npm uninstall react-ws-canvas --save && yalc add react-ws-canvas && npm install 318 | ``` 319 | 320 | ## how this project was built 321 | 322 | ```sh 323 | yarn create react-app react-ws-canvas --template typescript 324 | ``` 325 | 326 | Because I need a library to publish and either a working example to test/debug the library project structured this way: 327 | 328 | - `/package.json` ( workspaces "example" and "lib" ) 329 | - `/example` ( example and library sources ) 330 | - `/example/package.json` 331 | - `/example/.env` with BROWSER=none to avoid browser start when issue `yarn start` because I use F5 from vscode to open debugging session 332 | - `/example/lib` ( library source codes ) 333 | - `/lib` ( library publishing related files ) 334 | - `/lib/package.json` 335 | - `/lib/rollup.config.json` ( specifically `input: '../example/src/lib/index.tsx', ` ) 336 | - `/lib/tsconfig.json` ( specifically `"rootDirs": ["../example/src/lib"],` and `"include": ["../example/src/lib"],` ) 337 | - `/lib/prepatch-and-publish` ( helper script to prepatch version and publish with README.md ) 338 | 339 | ## development notes 340 | 341 | - **key notes** (old) 342 | - [cs.width and cs.height][1508] variables represents [canvas size][1509] and used as starting point for [calculations][1510] 343 | - [stateNfo, viewMap, overridenRowHeight][1500] are kept separate because stateNfo must light because frequently updated while viewMap and overridenRowHeight can be heavy struct for large grids 344 | - [view][1501] mapping have a size less-or-equal than rowsCount depending on [filtering][1502] and the order of view may different depending on sorting 345 | - some methods work with [real cells][1503] ( the same as user configure through getCellData, setCellData ), some other methods works with [view cells][1504] that is an internal representation convenient to deal on what-you-see-is-what-you-get 346 | - [real and view coords][1505] can be transformed between 347 | - [mkstates][1506] is an helper used primarly by [handlers][1507] to ensure callback uses synced state 348 | - **TODO** 349 | - type 'select' for cell with combobox integrated 350 | - date picker when F2 (or double click) on date type cell 351 | - isOverCell should true on last row when showPartialRows 352 | - deployment 353 | - remove any `from "react-ws-canvas";` 354 | 355 | [1500]: https://github.com/devel0/react-ws-canvas/blob/1c0200495ec75a6fe31f467884b9ac79b1e88ad6/example/src/lib/WSCanvas.tsx#L140-L146 356 | [1501]: https://github.com/devel0/react-ws-canvas/blob/1c0200495ec75a6fe31f467884b9ac79b1e88ad6/example/src/lib/WSCanvas.tsx#L159-L187 357 | [1502]: https://github.com/devel0/react-ws-canvas/blob/1c0200495ec75a6fe31f467884b9ac79b1e88ad6/example/src/lib/WSCanvas.tsx#L382 358 | [1503]: https://github.com/devel0/react-ws-canvas/blob/1c0200495ec75a6fe31f467884b9ac79b1e88ad6/example/src/lib/WSCanvas.tsx#L1053 359 | [1504]: https://github.com/devel0/react-ws-canvas/blob/1c0200495ec75a6fe31f467884b9ac79b1e88ad6/example/src/lib/WSCanvas.tsx#L648 360 | [1505]: https://github.com/devel0/react-ws-canvas/blob/1c0200495ec75a6fe31f467884b9ac79b1e88ad6/example/src/lib/WSCanvas.tsx#L183-L187 361 | [1506]: https://github.com/devel0/react-ws-canvas/blob/1c0200495ec75a6fe31f467884b9ac79b1e88ad6/example/src/lib/WSCanvas.tsx#L267 362 | [1507]: https://github.com/devel0/react-ws-canvas/blob/1c0200495ec75a6fe31f467884b9ac79b1e88ad6/example/src/lib/WSCanvas.tsx#L2039 363 | [1508]: https://github.com/devel0/react-ws-canvas/blob/dee24cfc21590793651abf21c6fecc9e32bc826d/example/src/lib/WSCanvas.tsx#L159-L169 364 | [1509]: https://github.com/devel0/react-ws-canvas/blob/5c1c29be00c6c3c4e55544df25e8c4087f0631a8/example/src/lib/WSCanvas.tsx#L2945-L2946 365 | [1510]: https://github.com/devel0/react-ws-canvas/blob/5c1c29be00c6c3c4e55544df25e8c4087f0631a8/example/src/lib/WSCanvas.tsx#L244 366 | -------------------------------------------------------------------------------- /doc/50000rows_200cols.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/50000rows_200cols.gif -------------------------------------------------------------------------------- /doc/api.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/api.gif -------------------------------------------------------------------------------- /doc/cell-style-customization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/cell-style-customization.png -------------------------------------------------------------------------------- /doc/clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/clipboard.png -------------------------------------------------------------------------------- /doc/column-width-resize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/column-width-resize.png -------------------------------------------------------------------------------- /doc/custom-color-picker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/custom-color-picker.gif -------------------------------------------------------------------------------- /doc/custom-combobox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/custom-combobox.gif -------------------------------------------------------------------------------- /doc/frozen-rows-cols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/frozen-rows-cols.png -------------------------------------------------------------------------------- /doc/initial-sort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/initial-sort.png -------------------------------------------------------------------------------- /doc/material-select-multi.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/material-select-multi.gif -------------------------------------------------------------------------------- /doc/quickstart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/quickstart.png -------------------------------------------------------------------------------- /doc/selection-mode-cells.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/selection-mode-cells.png -------------------------------------------------------------------------------- /doc/selection-mode-multi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/selection-mode-multi.png -------------------------------------------------------------------------------- /doc/selection-mode-rows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/selection-mode-rows.png -------------------------------------------------------------------------------- /doc/text-wrap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/text-wrap.gif -------------------------------------------------------------------------------- /doc/text-wrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/doc/text-wrap.png -------------------------------------------------------------------------------- /example/.env: -------------------------------------------------------------------------------- 1 | BROWSER=none -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ws-canvas-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@date-io/date-fns": "1.x", 7 | "@material-ui/core": "^4.8.3", 8 | "@material-ui/pickers": "^3.2.10", 9 | "@types/jest": "24.0.21", 10 | "@types/lodash": "^4.14.146", 11 | "@types/moment": "^2.13.0", 12 | "@types/node": "12.12.5", 13 | "@types/react": "16.9.11", 14 | "@types/react-color": "^3.0.1", 15 | "@types/react-dom": "16.9.3", 16 | "@types/react-router-dom": "^5.1.3", 17 | "copy-to-clipboard": "^3.2.0", 18 | "date-fns": "^2.9.0", 19 | "lodash": "^4.17.15", 20 | "moment": "2.24.0", 21 | "react": "^16.11.0", 22 | "react-color": "^2.18.0", 23 | "react-dom": "^16.11.0", 24 | "react-scripts": "3.2.0", 25 | "typescript": "3.6.4" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/example/public/logo192.png -------------------------------------------------------------------------------- /example/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devel0/react-ws-canvas/3b8a813551f5d9f76b986d39cfde6b29f71e401b/example/public/logo512.png -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /example/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "infiniteLoopProtection": false, 3 | "hardReloadOnChange": false, 4 | "view": "browser" 5 | } -------------------------------------------------------------------------------- /example/src/App.quickstart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { WSCanvas, useWindowSize, WSCanvasColumnClickBehavior } from './lib'; 3 | 4 | const AppQuickStart: React.FC = () => { 5 | const [rows, setRows] = useState([]); 6 | const winSize = useWindowSize(); 7 | 8 | const ROWS = 500000; 9 | const COLS = 20; 10 | 11 | useEffect(() => { 12 | 13 | const _rows = []; 14 | for (let ri = 0; ri < ROWS; ++ri) { 15 | const row = []; 16 | for (let ci = 0; ci < COLS; ++ci) { 17 | row.push("r:" + ri + " c:" + ci); 18 | } 19 | _rows.push(row); 20 | } 21 | 22 | setRows(_rows); 23 | }, []); 24 | 25 | return row[colIdx]} 32 | prepareCellDataset={() => rows.slice()} 33 | commitCellDataset={(q) => setRows(q)} 34 | rowSetCellData={(row, colIdx, value) => row[colIdx] = value} 35 | />; 36 | } 37 | 38 | export default AppQuickStart; -------------------------------------------------------------------------------- /example/src/Frame.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import AppQuickStart from './App.quickstart'; 3 | import { Sample1 } from './Sample1'; 4 | import { Sample2 } from './Sample2'; 5 | import { Sample3 } from './Sample3'; 6 | import { Sample4 } from './Sample4'; 7 | import { Sample5 } from './Sample5'; 8 | import { Sample6 } from './Sample6'; 9 | 10 | const EXAMPLE_NOTES = [ 11 | "500000 x 20 grid with minimal props", 12 | "50000 x 200 grid with frozen row/col, filter, custom column width", 13 | "5000 x 6 grid db-record-like column mapping, initial sort, custom sort, api onMouseDown, global filter, cell changing/changed", 14 | "5000 x 7 grid db-record-like, data interact del/change row, custom cell editor, rowHover", 15 | "add/insert/del/move/currentRealRowSel rows using api", 16 | "custom multi select with material-ui ; custom render chip with color picker", 17 | "resetView behavior to force sync ds ; resetSorting and resetFilters resetView arguments" 18 | ]; 19 | 20 | const DEFAULT_EXAMPLE = 5; 21 | 22 | export default function Frame() { 23 | const [example, setExample] = useState(DEFAULT_EXAMPLE); 24 | const [ctl, setCtl] = useState(null); 25 | const [notes, setNotes] = useState(EXAMPLE_NOTES[DEFAULT_EXAMPLE]); 26 | 27 | useEffect(() => { 28 | switch (example) { 29 | case 0: setCtl(); break; 30 | case 1: setCtl(); break; 31 | case 2: setCtl(); break; 32 | case 3: setCtl(); break; 33 | case 4: setCtl(); break; 34 | case 5: setCtl(); break; 35 | case 6: setCtl(); break; 36 | default: setCtl(
invalid example selection
); break; 37 | } 38 | }, [example]); 39 | 40 | return
41 | 42 |
43 | EXAMPLES 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 | 56 |
57 | 58 | EX{example} : {notes} 59 | 60 |
61 | {ctl} 62 |
63 |
64 | 65 | {/*
66 |

TEST DIV AFTER CONTROL

67 |

sample text 1

68 |

sample text 2

69 |

sample text 3

70 |

sample text 1

71 |

sample text 2

72 |

sample text 3

73 |

sample text 1

74 |

sample text 2

75 |

sample text 3

76 |
*/} 77 |
78 | } 79 | -------------------------------------------------------------------------------- /example/src/Sample1.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { WSCanvas, useWindowSize } from "./lib"; 3 | 4 | export function Sample1() { 5 | const [rows, setRows] = useState([]); 6 | const winSize = useWindowSize(); 7 | 8 | const ROWS = 50000; 9 | const COLS = 200; 10 | 11 | useEffect(() => { 12 | 13 | const _rows = []; 14 | for (let ri = 0; ri < ROWS; ++ri) { 15 | const row = []; 16 | for (let ci = 0; ci < COLS; ++ci) { 17 | row.push("r:" + ri + " c:" + ci); 18 | } 19 | _rows.push(row); 20 | } 21 | 22 | setRows(_rows); 23 | }, []); 24 | 25 | return
26 | row[colIdx]} 33 | prepareCellDataset={() => rows.slice()} 34 | commitCellDataset={(q) => setRows(q)} 35 | rowSetCellData={(row, colIdx, value) => row[colIdx] = value} 36 | colWidth={(ci) => { 37 | let w = 50; 38 | for (let i = 0; i < ci; ++i) { 39 | w += 50; 40 | if (w > 200) w = 50; 41 | } 42 | return w; 43 | }} 44 | rowsCount={rows.length} colsCount={COLS} 45 | showRowNumber={true} showColNumber={true} showFilter={true} 46 | frozenRowsCount={1} frozenColsCount={1} 47 | /> 48 |
49 | } 50 | -------------------------------------------------------------------------------- /example/src/Sample2.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvas, WSCanvasColumn, WSCanvasSortDirection, useWindowSize, setFieldData, WSCanvasSelectMode, WSCanvasApi } from "./lib"; 2 | 3 | import React, { useState, useEffect } from "react"; 4 | import * as _ from 'lodash'; 5 | 6 | interface MyData { 7 | col1: string; 8 | col2: number; 9 | col3: boolean; 10 | col4: Date; 11 | col5: Date; 12 | col6: Date; 13 | } 14 | 15 | export function Sample2() { 16 | const [rows, setRows] = useState([]); 17 | const winSize = useWindowSize(); 18 | const [useGlobalFilter, setUseGlobalFilter] = useState(false); 19 | const [logEditingToConsole, setLogEditingToConsole] = useState(false); 20 | const [api, setapi] = useState(null); 21 | 22 | const ROWS = 5000; 23 | 24 | const columns = [ 25 | { 26 | type: "text", 27 | header: "col1", 28 | field: "col1", 29 | textAlign: () => "center", 30 | lessThan: (a, b) => { 31 | const aNr = parseInt((a as string).replace("r", "")); 32 | const bNr = parseInt((b as string).replace("r", "")); 33 | 34 | return aNr < bNr; 35 | }, 36 | sortDirection: WSCanvasSortDirection.Descending, 37 | sortOrder: 1, 38 | width: 120, 39 | }, 40 | { 41 | type: "number", 42 | header: "col2", 43 | field: "col2", 44 | lessThan: (a, b) => (a as number) < (b as number), 45 | sortDirection: WSCanvasSortDirection.Ascending, 46 | sortOrder: 0, 47 | }, 48 | { 49 | type: "boolean", 50 | header: "col3", 51 | field: "col3", 52 | onChanging: (states, row, cell, oldValue, newValue) => { 53 | // set to false to inhibit editing 54 | return true; 55 | }, 56 | onChanged: (states, _row, cell, oldValue, newValue) => { 57 | const row = _row as MyData; 58 | console.log("changed boolean column from [" + String(oldValue) + "] to [" + String(row.col3) + "]"); 59 | }, 60 | }, 61 | { 62 | type: "date", 63 | header: "col4", 64 | field: "col4", 65 | lessThan: (a, b) => (a as Date) < (b as Date) 66 | }, 67 | { 68 | type: "time", 69 | header: "col5", 70 | field: "col5" 71 | }, 72 | { 73 | type: "datetime", 74 | header: "col6", 75 | field: "col6", 76 | }, 77 | ] as WSCanvasColumn[]; 78 | 79 | useEffect(() => { 80 | console.log("GENERATE DATA"); 81 | 82 | const _rows: MyData[] = []; 83 | for (let ri = 0; ri < ROWS; ++ri) { 84 | _rows.push({ 85 | col1: "r" + ri, 86 | col2: Math.trunc(ri / 4) * 10, 87 | col3: ri % 2 === 0, 88 | col4: new Date(new Date().getTime() + (ri * 24 * 60 * 60 * 1000)), // +1 day 89 | col5: new Date(new Date().getTime() + (ri * 60 * 1000)), // +1 min 90 | col6: new Date(new Date().getTime() + (ri * 24 * 60 * 60 * 1000 + ri * 60 * 1000)), // +1 day +1min 91 | }); 92 | } 93 | 94 | setRows(_rows); 95 | }, []); 96 | 97 | useEffect(() => { 98 | if (api) api.resetView(); 99 | }, [useGlobalFilter]); 100 | 101 | return
102 | 103 | { setUseGlobalFilter(e.target.checked) }} /> 104 | 105 | 106 | 107 | { setLogEditingToConsole(e.target.checked) }} /> 108 | 109 | 110 | row[columns[colIdx].field]} 115 | // getCellData={(cell) => (rows[cell.row] as any)[columns[cell.col].field]} 116 | prepareCellDataset={() => rows.slice()} 117 | commitCellDataset={(q) => setRows(q)} 118 | rowSetCellData={(row, colIdx, value) => setFieldData(row, columns[colIdx].field, value)} 119 | 120 | fullwidth height={winSize.height * .8} 121 | containerStyle={{ margin: "1em" }} 122 | rowHoverColor={(row, ridx) => "rgba(127,127,127, 0.1)"} 123 | // rowHoverColor={(row, ridx) => { 124 | // if (ridx !== 2) return "rgba(248,248,248,1)"; 125 | // }} 126 | globalFilter={(_row, ridx) => { 127 | if (!useGlobalFilter) return undefined; 128 | 129 | const row = _row as MyData; 130 | 131 | return row.col1.indexOf("1") !== -1; 132 | }} 133 | getCellBackgroundColor={(row, cell) => { 134 | if (cell.row === 2) return "cyan"; 135 | if (cell.col === 1) return "lightyellow"; 136 | }} 137 | getCellFont={(row, cell, props) => { 138 | if (cell.col === 1) return "bold " + props.font; 139 | }} 140 | getCellTextColor={(row, cell) => { 141 | if (cell.col === 2) return "green"; 142 | }} 143 | rowHeight={() => 30} 144 | selectionMode={WSCanvasSelectMode.Cell} 145 | showFilter={true} 146 | showColNumber={true} showRowNumber={true} 147 | colWidthExpand={false} 148 | frozenRowsCount={0} frozenColsCount={0} 149 | 150 | onApi={(api) => setapi(api)} 151 | onCellEditing={(states, row, cell, oldValue, newValue) => { 152 | // return false to inhibit editing 153 | return true; 154 | }} 155 | onCellEdited={(states, row, cell, oldValue, newValue) => { 156 | if (logEditingToConsole) console.log("changed cell " + cell.toString() + " from [" + oldValue + "] to [" + newValue + "]"); 157 | }} 158 | /> 159 |
160 | } -------------------------------------------------------------------------------- /example/src/Sample3.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | WSCanvas, WSCanvasColumn, WSCanvasSortDirection, WSCanvasSelectMode, mapEnum, 3 | WSCanvasApi, useWindowSize, WSCanvasStates, getFieldData, setFieldData 4 | } from "./lib"; 5 | 6 | import React, { useState, useEffect, useRef } from "react"; 7 | 8 | import * as _ from 'lodash'; 9 | 10 | enum MyEnum { 11 | first, 12 | second, 13 | third 14 | } 15 | 16 | interface MyDataNested { 17 | nfo: string; 18 | } 19 | 20 | interface MyData { 21 | col1: string; 22 | col2: number; 23 | col3: boolean; 24 | col4: Date; 25 | col5: Date; 26 | col6: Date; 27 | col7: string; 28 | cboxcol: MyEnum; 29 | nested: MyDataNested; 30 | } 31 | 32 | export function Sample3() { 33 | const [rows, setRows] = useState([]); 34 | const [api, setApi] = useState(null); 35 | const [gridStateNfo, setGridStateNfo] = useState({} as WSCanvasStates); 36 | const [overCellCoord, setOverCellCoord] = useState(""); 37 | const tooltipDivRef = useRef(null); 38 | const [tooltipTest, setTooltipTest] = useState(false); 39 | const winSize = useWindowSize(); 40 | const dbgDiv = useRef(null); 41 | const [readonly, setReadonly] = useState(false); 42 | 43 | const ROWS = 5000; 44 | 45 | const columns = [ 46 | { 47 | type: "text", 48 | header: "col1", 49 | field: "col1", 50 | lessThan: (a, b) => { 51 | if (a && b) { 52 | const aNr = parseInt((a as string).replace("r", "")); 53 | const bNr = parseInt((b as string).replace("r", "")); 54 | 55 | return aNr < bNr; 56 | } 57 | return -1; 58 | }, 59 | textAlign: () => "center", 60 | sortDirection: WSCanvasSortDirection.Ascending, 61 | sortOrder: 1, 62 | renderTransform: (row, cell, data) => "( " + data + " )", 63 | readonly: true, 64 | }, 65 | { 66 | type: "number", 67 | header: "col2", 68 | field: "col2", 69 | lessThan: (a, b) => (a as number) < (b as number), 70 | sortDirection: WSCanvasSortDirection.Descending, 71 | sortOrder: 0, 72 | }, 73 | { 74 | type: "boolean", 75 | header: "col3", 76 | field: "col3" 77 | }, 78 | { 79 | type: "date", 80 | header: "col4", 81 | field: "col4", 82 | lessThan: (a, b) => (a as Date) < (b as Date) 83 | }, 84 | { 85 | type: "time", 86 | header: "col5", 87 | field: "col5" 88 | }, 89 | { 90 | type: "text", 91 | header: "cbox", 92 | field: "cboxcol", 93 | renderTransform: (row, cell, data) => { 94 | const q = mapEnum(MyEnum).find((x) => x.value === data); 95 | if (q) return q.name; 96 | }, 97 | customEdit: (states, row, cell, containerStyle?, cellWidth?, cellHeight?) => { 98 | if (containerStyle) containerStyle.background = "lightyellow"; 99 | 100 | return
101 | 140 |
141 | } 142 | }, 143 | { 144 | type: "datetime", 145 | header: "col6", 146 | field: "col6" 147 | }, 148 | { 149 | type: "text", 150 | header: "description", 151 | field: "col7", 152 | wrapText: true, 153 | }, 154 | { 155 | type: "text", 156 | header: "nested data", 157 | field: "nested.nfo", 158 | }, 159 | ] as WSCanvasColumn[]; 160 | 161 | const newObj = (ri: number) => { 162 | return { 163 | col1: "r" + ri, 164 | col2: Math.trunc(ri / 4) * 10, 165 | col3: ri % 2 === 0, 166 | col4: new Date(new Date().getTime() + (ri * 24 * 60 * 60 * 1000)), // +1 day 167 | col5: new Date(new Date().getTime() + (ri * 60 * 1000)), // +1 min 168 | col6: new Date(new Date().getTime() + (ri * 24 * 60 * 60 * 1000 + ri * 60 * 1000)), // +1 day +1min 169 | col7: ri % 2 === 0 ? "short text" : "long text that will be wrapped because too long", 170 | cboxcol: (ri % 3) as MyEnum, 171 | nested: { 172 | nfo: "nested nfo:" + ri 173 | } 174 | } as MyData; 175 | } 176 | 177 | useEffect(() => { 178 | console.log("GENERATE DATA"); 179 | 180 | const _rows: MyData[] = []; 181 | for (let ri = 0; ri < ROWS; ++ri) { 182 | _rows.push(newObj(ri)); 183 | } 184 | 185 | setRows(_rows); 186 | }, []); 187 | 188 | return
189 | 194 | 195 | 199 | 200 | 205 | 206 | 209 | 210 | 214 | 215 | { setReadonly(e.target.checked) }} /> 216 | 217 | 218 | 219 | gridStateNfo:{(gridStateNfo && gridStateNfo.state && gridStateNfo.state.focusedCell) ? gridStateNfo.state.focusedCell.toString() : ""} 220 |
221 |
222 | 223 | {tooltipTest ? 224 |
225 | 226 | 228 | 229 |
230 | TIP for cell: {overCellCoord} 231 |
232 |
: null} 233 | 234 | { 239 | const fieldname = columns[colIdx].field; 240 | return getFieldData(row, fieldname); 241 | }} 242 | prepareCellDataset={() => rows.slice()} 243 | commitCellDataset={(q) => setRows(q)} 244 | rowSetCellData={(row, colIdx, value) => { 245 | const fieldname = columns[colIdx].field; 246 | if (row) setFieldData(row, fieldname, value); 247 | }} 248 | 249 | containerStyle={{ margin: "1em" }} 250 | fullwidth 251 | height={Math.max(300, winSize.height * .8)} 252 | frozenRowsCount={0} frozenColsCount={0} 253 | colWidthExpand={true} 254 | showFilter={true} 255 | showPartialColumns={true} showPartialRows={true} 256 | showColNumber={true} showRowNumber={true} 257 | rowHoverColor={(row, ridx) => "rgba(127,127,127, 0.1)"} 258 | rowHeight={() => 35} textMargin={5} 259 | selectionMode={WSCanvasSelectMode.Cell} 260 | 261 | onStateChanged={(states) => setGridStateNfo(states)} 262 | getCellTextColor={(row, coord, props) => { 263 | if (props.isCellReadonly && props.isCellReadonly(row, coord)) return "gray"; 264 | return undefined; 265 | }} 266 | isCellReadonly={(row, coord) => { 267 | return readonly; 268 | }} 269 | onApi={(api) => setApi(api)} 270 | onMouseDown={(states, e, cell) => { 271 | if (cell) { 272 | if (cell.row >= 0) { 273 | const data = rows[cell.row] as MyData; 274 | if (data) console.log("clicked cell row:" + cell.row + " col1data:" + data.col1); 275 | } 276 | } 277 | }} 278 | onMouseOverCell={(states, nfo) => { 279 | if (tooltipTest) { 280 | if (api && tooltipDivRef && tooltipDivRef.current) { 281 | const div = tooltipDivRef.current; 282 | if (nfo && nfo.cell.row >= 0 && nfo.cell.col >= 0) { 283 | const canvasCoord = api.canvasCoord(); 284 | const cellCanvasCoord = api.cellToCanvasCoord(nfo.cell); 285 | if (canvasCoord && cellCanvasCoord) { 286 | setOverCellCoord(nfo.cell.toString() + " canvas:" + cellCanvasCoord.toString()); 287 | div.style["left"] = (canvasCoord.x + cellCanvasCoord.x + cellCanvasCoord.width) + "px"; 288 | div.style["top"] = (canvasCoord.y + cellCanvasCoord.y + cellCanvasCoord.height / 2) + "px"; 289 | 290 | // div.style["left"] = (nfo.xy[0] + 25) + "px"; 291 | // div.style["top"] = (nfo.xy[1] + 25) + "px"; 292 | 293 | div.style["display"] = "block"; 294 | } 295 | } else { 296 | div.style["display"] = "none"; 297 | } 298 | } 299 | } 300 | }} 301 | /> 302 |
303 | } -------------------------------------------------------------------------------- /example/src/Sample4.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { 3 | WSCanvas, WSCanvasColumn, WSCanvasSortDirection, WSCanvasApi, useWindowSize, 4 | WSCanvasStates, WSCanvasColumnClickBehavior, WSCanvasCellCoord, WSCanvasSelection, WSCanvasSelectionRange, setFieldData, getFieldData 5 | } from "./lib"; 6 | import * as _ from 'lodash'; 7 | 8 | interface MyData { 9 | idx: number; 10 | description: string; 11 | timestamp: Date; 12 | } 13 | 14 | class IUpdateEntityNfo { 15 | constructor(current: T, original?: T) { 16 | this._current = current; 17 | if (original) 18 | this._original = original; 19 | else 20 | this._original = _.cloneDeep(current); 21 | } 22 | _current: T; 23 | _original: T; 24 | 25 | get current() { return this._current; } 26 | get original() { return this._original; } 27 | } 28 | 29 | export function Sample4() { 30 | const [ds, setDs] = useState>(new IUpdateEntityNfo([], [])); 31 | const [api, setApi] = useState(null); 32 | const [gridStates, setGridState] = useState(null); 33 | const [dirty, setDirty] = useState(false); 34 | const winSize = useWindowSize(); 35 | const [colVisible, setColVisible] = useState(true); 36 | const [columns, setColumns] = useState([]); 37 | const [selectedRealRowIdxs, setSelectedRealRowIdx] = useState([]); 38 | 39 | useEffect(() => { 40 | const cols = [ 41 | { 42 | type: "text", 43 | header: "ridx", 44 | field: "ridx", 45 | width: 100, 46 | textAlign: () => "center", 47 | renderTransform: (row, cell, value) => cell.row, 48 | //hidden: true, 49 | }, 50 | { 51 | type: "number", 52 | header: "vidx", 53 | field: "idx", 54 | width: 100, 55 | sortOrder: 0, 56 | sortDirection: WSCanvasSortDirection.Ascending, 57 | lessThan: (a, b) => a < b, 58 | textAlign: () => "center", 59 | hidden: !colVisible, 60 | }, 61 | { 62 | type: "text", 63 | header: "Description", 64 | field: "description", 65 | width: 100, 66 | }, 67 | { 68 | type: "text", 69 | header: "Last modify", 70 | field: "timestamp", 71 | width: 250, 72 | readonly: true, 73 | renderTransform: (_row, cell, value) => { 74 | const row = _row as MyData; 75 | if (row) { 76 | if (api && row.timestamp) { 77 | return api.formatCellDataAsDateTime(row.timestamp) + " (custom user) " + row.timestamp.getTime(); 78 | } 79 | return ""; 80 | } 81 | return undefined; 82 | }, 83 | } 84 | ] as WSCanvasColumn[]; 85 | 86 | setColumns(cols); 87 | }, [colVisible, api]); 88 | 89 | useEffect(() => { 90 | const qcur = JSON.stringify(ds.current); 91 | const qorig = JSON.stringify(ds.original); 92 | if (qorig !== qcur && !dirty) { 93 | setDirty(true); 94 | } else if (qorig === qcur && dirty) { 95 | setDirty(false); 96 | } 97 | }, [ds, dirty]); 98 | 99 | useEffect(() => { 100 | const newset = new IUpdateEntityNfo([{ 101 | idx: 0, 102 | description: "test", 103 | timestamp: new Date() 104 | }]); 105 | setDs(newset); 106 | }, []); 107 | 108 | const addRow = () => { 109 | const newset = new IUpdateEntityNfo(ds.current.slice()); 110 | newset.current.push({ 111 | idx: ds.current.length > 0 ? _.max(ds.current.map((r) => r.idx))! + 1 : 0, 112 | description: "test" + ds.current.length, 113 | timestamp: new Date() 114 | }); 115 | setDs(newset); 116 | 117 | if (api && gridStates) { 118 | api.begin(); 119 | const cell = new WSCanvasCellCoord(newset.current.length - 1, 2); 120 | api.focusCell(cell); 121 | api.commit(); 122 | } 123 | } 124 | 125 | const insRow = () => { 126 | if (api) { 127 | api.begin(); 128 | 129 | api.prepareCellDataset(); 130 | 131 | const focusedCell = api.states.state.focusedCell; 132 | 133 | // see prepareDataset type 134 | const dset = api.ds as MyData[]; 135 | 136 | const row_idx_new = dset[focusedCell.row].idx; 137 | const focusedViewCell = api.realCellToView(focusedCell); 138 | let vri = focusedViewCell.row; 139 | while (vri < api.states.state.filteredSortedRowsCount) { 140 | const rowi = dset[api.viewRowToRealRow(vri)]; 141 | rowi.idx++; 142 | ++vri; 143 | } 144 | const newRowData = { 145 | idx: row_idx_new, 146 | description: "INSERTED test" + dset.length, 147 | timestamp: new Date() 148 | } as MyData; 149 | dset.push(newRowData); 150 | 151 | api.commitCellDataset(); 152 | 153 | api.commit(true); 154 | } 155 | } 156 | 157 | const delRow = () => { 158 | if (api) { 159 | api.begin(); 160 | 161 | const viewRows = api.states.state.viewSelection.rowIdxs(); 162 | const idxsToRemove: number[] = []; 163 | viewRows.forEach((viewRow) => { 164 | const rowIdx = api.viewRowToRealRow(viewRow); 165 | idxsToRemove.push(rowIdx); 166 | }); 167 | 168 | idxsToRemove.sort((a, b) => b - a); 169 | 170 | api.prepareCellDataset(); 171 | const newset = api.ds as MyData[]; 172 | 173 | const arr = newset; 174 | for (let i = 0; i < idxsToRemove.length; ++i) { 175 | arr.splice(idxsToRemove[i], 1); 176 | } 177 | api.commitCellDataset(); 178 | 179 | api.commit(); 180 | } 181 | } 182 | 183 | const moveRow = (condition: (focusedViewCellRow: number) => boolean, delta: number) => { 184 | if (api) { 185 | const focusedCell = api.states.state.focusedCell; 186 | const focusedViewCell = api.realCellToView(focusedCell); 187 | 188 | if (condition(focusedViewCell.row)) { 189 | api.begin(); 190 | 191 | const viewCellUpper = focusedViewCell.setRow(focusedViewCell.row + delta); 192 | const cellUpper = api.viewCellToReal(viewCellUpper); 193 | 194 | //const COL = columns.findIndex((x) => x.field === "idx"); // alternative 195 | 196 | //const qup = api.getCellData(new WSCanvasCellCoord(cellUpper.row, COL)); // alternative 197 | //const qthis = api.getCellData(new WSCanvasCellCoord(focusedCell.row, COL)); // alternative 198 | const qup = ds.current[cellUpper.row].idx; 199 | const qthis = ds.current[focusedCell.row].idx; 200 | 201 | api.prepareCellDataset(); 202 | //api.setCellData(new WSCanvasCellCoord(cellUpper.row, COL), qthis); // alternative 203 | //api.setCellData(new WSCanvasCellCoord(focusedCell.row, COL), qup); // alternative 204 | (api.ds[cellUpper.row] as MyData).idx = qthis; 205 | (api.ds[focusedCell.row] as MyData).idx = qup; 206 | api.commitCellDataset(); 207 | 208 | api.filter(); 209 | api.sort(); 210 | api.focusCell(api.viewCellToReal(viewCellUpper)); 211 | api.selectFocusedCell(); 212 | 213 | api.commit();; 214 | } 215 | } 216 | } 217 | 218 | const toggleCol = () => { 219 | setColVisible(!colVisible); 220 | if (api && gridStates) { api.resetView(); } 221 | } 222 | 223 | const selRealRow0 = () => { 224 | if (api) { 225 | api.begin(); 226 | const cell = new WSCanvasCellCoord(0, 0); 227 | api.setRealSelection(new WSCanvasSelection([new WSCanvasSelectionRange(cell)])); 228 | api.focusCell(cell); 229 | api.commit(); 230 | } 231 | } 232 | 233 | return
234 | 235 | focusedCell:{api ? api.states.state.focusedCell.toString() : ""} - focusedViewCell:{api ? api.realCellToView(api.states.state.focusedCell).toString() : ""} - rowsCount:{api ? api.states.props.rowsCount : -1} - filteredSortedrRowsCount:{api ? api.states.state.filteredSortedRowsCount : -1}
236 | viewSelection:{api ? api.states.state.viewSelection.toString() : ""}
237 | apiOverCell:{api ? String(api.states.state.cursorOverCell) : ""}
238 | realToView:{(api && api.states.vm) ? api.states.vm.realToView.join('-') : ""}
239 | viewToReal:{(api && api.states.vm) ? api.states.vm.viewToReal.join('-') : ""}
240 | selectedRealRowIdxs:{selectedRealRowIdxs.join(", ")}
241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | {api ? 251 | 254 | : null} 255 | 256 | {api ? 257 | 259 | : null} 260 | 261 | 262 | 263 | 264 | 265 | { 270 | if (row) return getFieldData(row, columns[colIdx].field); 271 | return ""; 272 | }} 273 | prepareCellDataset={() => ds.current.slice()} 274 | rowSetCellData={(row, colIdx, value) => { 275 | if (row) setFieldData(row, columns[colIdx].field, value); 276 | }} 277 | commitCellDataset={(q) => { setDs(new IUpdateEntityNfo(q, ds.original)); }} 278 | showFilter={true} 279 | 280 | containerStyle={{ marginTop: "1em" }} 281 | fullwidth 282 | height={Math.max(300, winSize.height * .4)} 283 | showColNumber={true} 284 | columnClickBehavior={WSCanvasColumnClickBehavior.None} 285 | 286 | debug={false} 287 | onApi={(api) => setApi(api)} 288 | onStateChanged={(states) => { 289 | if (api) setSelectedRealRowIdx(Array.from(api.getRealSelection().rowIdxs())); 290 | setGridState(states); 291 | }} 292 | /> 293 |
294 | } -------------------------------------------------------------------------------- /example/src/Sample5.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { FormControl, InputLabel, Select, MenuItem, Input, InputLabelProps, Chip } from '@material-ui/core'; 3 | import { 4 | WSCanvas, WSCanvasColumn, WSCanvasApi, useWindowSize, 5 | WSCanvasStates, WSCanvasColumnClickBehavior, WSCanvasCellCoord, setFieldData, getFieldData, pathBuilder 6 | } from "./lib"; 7 | import { DatePicker, MuiPickersUtilsProvider } from "@material-ui/pickers"; 8 | import DateFnsUtils from '@date-io/date-fns'; 9 | import * as _ from 'lodash'; 10 | import { SketchPicker, SwatchesPicker } from "react-color"; 11 | 12 | interface MyDataUser { 13 | id: number; 14 | username: string; 15 | } 16 | 17 | interface MyData { 18 | descr: string; 19 | users: MyDataUser[]; 20 | color: string; 21 | dt: Date; 22 | } 23 | 24 | const ITEM_HEIGHT = 48; 25 | const ITEM_PADDING_TOP = 8; 26 | const MenuProps = { 27 | PaperProps: { 28 | style: { 29 | maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, 30 | width: 250, 31 | }, 32 | }, 33 | }; 34 | 35 | // to specify field using secure path by intellisense 36 | const SP_MyData = pathBuilder(); 37 | 38 | export function Sample5() { 39 | const [ds, setDs] = useState([]); 40 | const [api, setApi] = useState(null); 41 | const [gridStates, setGridState] = useState(null); 42 | const winSize = useWindowSize(); 43 | const [colVisible] = useState(true); 44 | const [columns, setColumns] = useState([]); 45 | const [usersSelectOpened, setUsersSelectOpened] = useState(false); 46 | const [dtDlgOpened, setDtDlgOpened] = useState(false); 47 | 48 | const USERS_BASE: MyDataUser[] = []; 49 | for (let i = 1; i <= 10; ++i) USERS_BASE.push( 50 | { 51 | id: i, 52 | username: "user" + i 53 | }); 54 | 55 | useEffect(() => { 56 | const cols = [ 57 | { 58 | type: "text", 59 | header: "description", 60 | field: SP_MyData("descr"), 61 | width: 20, 62 | textAlign: () => "center", 63 | }, 64 | { 65 | type: "text", 66 | header: "users", 67 | field: SP_MyData("users"), 68 | width: 100, 69 | renderTransform: (_row, cell, value) => { 70 | const row = _row as MyData; 71 | if (value) { 72 | const users = row.users; 73 | 74 | return users.map(x => x.username).join(","); 75 | } 76 | }, 77 | customEdit: (states, _row) => { 78 | const row = _row as MyData; 79 | 80 | return
81 | 82 | Users set 83 | } 112 | MenuProps={MenuProps} 113 | > 114 | {USERS_BASE.map(x => ( 115 | 116 | {x.username} 117 | 118 | ))} 119 | 120 | 121 |
122 | } 123 | }, 124 | { 125 | type: "text", 126 | header: "color", 127 | field: SP_MyData("color"), 128 | width: 100, 129 | customRender: (states, _row) => { 130 | const row = _row as MyData; 131 | return 132 | }, 133 | customEdit: (states, _row) => { 134 | const row = _row as MyData; 135 | 136 | return
137 | { 140 | 141 | }} 142 | onChange={(x) => { 143 | if (api) { 144 | api.begin(); 145 | api.setCustomEditValue(x.hex); 146 | api.commit(); 147 | api.closeCustomEdit(true); 148 | } 149 | }} /> 150 |
151 | } 152 | }, 153 | { 154 | // https://material-ui-pickers.dev/getting-started/installation 155 | type: "date", 156 | header: "date", 157 | field: SP_MyData("dt"), 158 | width: 100, 159 | 160 | customEdit: (states, row) => { 161 | const r = row as MyData; 162 | 163 | setDtDlgOpened(true); 164 | 165 | return 166 | { 171 | if (api) api.closeCustomEdit(true); 172 | setDtDlgOpened(false); 173 | }} 174 | onAbort={() => { 175 | if (api) api.closeCustomEdit(false); 176 | setDtDlgOpened(false); 177 | }} 178 | onClose={() => { 179 | if (api) api.closeCustomEdit(false); 180 | setDtDlgOpened(false); 181 | }} 182 | onChange={date => { 183 | if (api) { 184 | api.setCustomEditValue(date); 185 | } 186 | }} 187 | animateYearScrolling 188 | /> 189 | 190 | } 191 | } 192 | ] as WSCanvasColumn[]; 193 | 194 | setColumns(cols); 195 | }, [colVisible, api]); 196 | 197 | const addRow = () => { 198 | const newset = ds.slice(); 199 | newset.push({ 200 | descr: "test" + ds.length, 201 | dt: new Date(), 202 | color: "#ff0000", 203 | users: [] 204 | }); 205 | setDs(newset); 206 | 207 | if (api && gridStates) { 208 | api.begin(); 209 | const cell = new WSCanvasCellCoord(newset.length - 1, 0); 210 | api.focusCell(cell); 211 | api.commit(); 212 | } 213 | } 214 | 215 | useEffect(() => { 216 | addRow(); 217 | }, []); 218 | 219 | return
220 | selected rows count: {(api ? api.states.state.selectedRowsCount : 0)} 221 | description: {((api && api.states.state.selectedRow) ? (api.states.state.selectedRow as MyData).descr : "")} 222 |

223 | 224 | 225 | 226 | { 231 | if (row) return getFieldData(row, columns[colIdx].field); 232 | return ""; 233 | }} 234 | prepareCellDataset={() => ds.slice()} 235 | rowSetCellData={(row, colIdx, value) => { 236 | if (row) setFieldData(row, columns[colIdx].field, value); 237 | }} 238 | // rowHeight={() => 50} 239 | commitCellDataset={() => { setDs(ds); }} 240 | showFilter={true} 241 | 242 | containerStyle={{ marginTop: "1em" }} 243 | fullwidth 244 | height={Math.max(300, winSize.height * .4)} 245 | showColNumber={true} 246 | columnClickBehavior={WSCanvasColumnClickBehavior.None} 247 | 248 | debug={false} 249 | onApi={(api) => setApi(api)} 250 | onCustomEdit={() => { setUsersSelectOpened(true); }} 251 | onStateChanged={(states) => setGridState(states)} 252 | /> 253 |
254 | } -------------------------------------------------------------------------------- /example/src/Sample6.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { WSCanvas, useWindowSize, WSCanvasColumnClickBehavior, WSCanvasColumnSortInfo, WSCanvasSortDirection, WSCanvasApi } from './lib'; 3 | 4 | export const Sample6 = () => { 5 | const ROWS = 10; 6 | const COLS = 10; 7 | 8 | const rndSrc = () => { 9 | const _rows = []; 10 | for (let ri = 0; ri < ROWS; ++ri) { 11 | const row = []; 12 | for (let ci = 0; ci < COLS; ++ci) { 13 | row.push(Math.trunc(Math.random() * ROWS)); 14 | } 15 | _rows.push(row); 16 | } 17 | return _rows; 18 | }; 19 | const [rows, setRows] = useState(rndSrc()); 20 | const winSize = useWindowSize(); 21 | const [api, setapi] = useState(null); 22 | const [useReset, setUseReset] = useState(true); 23 | const [resetColumnSorting, setResetColumnSorting] = useState(false); 24 | const [resetColumnFilters, setResetColumnFilters] = useState(false); 25 | 26 | const setRndDatasource = () => { 27 | setRows(rndSrc()); 28 | // reset the view to inform the grid that an initial sort required 29 | // elsewhere grid tought it was an editing effect and doesn't apply sorting 30 | if (api && useReset) api.resetView(resetColumnSorting, resetColumnFilters); // UNCOMMENT THIS TO SEE UNSORT data after "set datasource" 2th button click 31 | }; 32 | 33 | return
34 | 37 | 38 | 39 | { setUseReset(e.target.checked) }} /> 40 | 41 | 42 | 43 | 44 | { setResetColumnSorting(e.target.checked) }} /> 45 | 46 | 47 | 48 | 49 | { setResetColumnFilters(e.target.checked) }} /> 50 | 51 | 52 | 53 | row[colIdx]} 61 | prepareCellDataset={() => rows.slice()} 62 | commitCellDataset={(q) => setRows(q)} 63 | rowSetCellData={(row, colIdx, value) => row[colIdx] = value} 64 | onApi={(api) => setapi(api)} 65 | columnInitialSort={[ 66 | { 67 | columnIndex: 0, 68 | sortDirection: WSCanvasSortDirection.Ascending 69 | } as WSCanvasColumnSortInfo 70 | ] as WSCanvasColumnSortInfo[]} 71 | /> 72 |
73 | } -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import * as serviceWorker from './serviceWorker'; 5 | import { Sample3 } from './Sample3'; 6 | import Frame from './Frame'; 7 | 8 | ReactDOM.render( 9 | , 10 | document.getElementById('root')); 11 | 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /example/src/lib/Utils.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useLayoutEffect, useCallback } from 'react'; 2 | 3 | /** from Chris West's Blog ( http://cwestblog.com/2013/09/05/javascript-snippet-convert-number-to-column-name/ ) */ 4 | export function toColumnName(num: number) { 5 | for (var ret = '', a = 1, b = 26; (num -= a) >= 0; a = b, b *= 26) { 6 | ret = String.fromCharCode(parseInt(String((num % b) / a)) + 65) + ret; 7 | } 8 | return ret; 9 | } 10 | 11 | export default function useDebounce(value: any, ms: number) { 12 | const [debouncedValue, setDebouncedValue] = useState(value); 13 | 14 | useEffect(() => { 15 | const handler = setTimeout(() => setDebouncedValue(value), ms); 16 | 17 | return () => clearTimeout(handler); 18 | }, [value, ms]); 19 | 20 | return debouncedValue; 21 | } 22 | 23 | export function getComputedStyleMarginPadding(elt: Element) { 24 | const csty = getComputedStyle(elt); 25 | 26 | const marginLeft = csty.marginLeft ? parseFloat(csty.marginLeft) : 0; 27 | const marginRight = csty.marginRight ? parseFloat(csty.marginRight) : 0; 28 | const marginTop = csty.marginTop ? parseFloat(csty.marginTop) : 0; 29 | const marginBottom = csty.marginBottom ? parseFloat(csty.marginBottom) : 0; 30 | 31 | const paddingLeft = csty.paddingLeft ? parseFloat(csty.paddingLeft) : 0; 32 | const paddingRight = csty.paddingRight ? parseFloat(csty.paddingRight) : 0; 33 | const paddingTop = csty.paddingTop ? parseFloat(csty.paddingTop) : 0; 34 | const paddingBottom = csty.paddingBottom ? parseFloat(csty.paddingBottom) : 0; 35 | 36 | const margin_padding_W = (marginLeft + marginRight + paddingLeft + paddingRight); 37 | const margin_padding_H = (marginTop + marginBottom + paddingTop + paddingBottom); 38 | 39 | return [margin_padding_W, margin_padding_H]; 40 | } 41 | 42 | /** hook to retrieve sum or margin(left+right) and padding(left+right) of div element; optionally consider parent's */ 43 | export function useDivMarginPadding(div: React.RefObject, considerParent: boolean = false) { 44 | const [val, setVal] = useState([0, 0]); 45 | useEffect(() => { 46 | if (div.current) { 47 | let res = [0, 0]; 48 | 49 | const q = getComputedStyleMarginPadding(div.current); 50 | res[0] += q[0]; 51 | res[1] += q[1]; 52 | 53 | if (considerParent) { 54 | let p = div.current.parentElement; 55 | while (p) { 56 | const qp = getComputedStyleMarginPadding(p); 57 | 58 | res[0] += qp[0]; 59 | res[1] += qp[1]; 60 | 61 | p = p.parentElement; 62 | } 63 | } 64 | 65 | setVal(res); 66 | } 67 | }, [div, considerParent]); 68 | 69 | return val; 70 | } 71 | 72 | export interface GraphicsSize { 73 | width: number; 74 | height: number; 75 | } 76 | 77 | export function getWindowSize() { 78 | return { 79 | width: window.innerWidth, 80 | height: window.innerHeight 81 | } as GraphicsSize; 82 | } 83 | 84 | export function isMobile() { 85 | const agent = navigator.userAgent; 86 | return agent.match(/Android/i) || 87 | agent.match(/iPhone|iPad|IPod/i) || 88 | agent.match(/Opera Mini/i) || 89 | agent.match(/IEMobile/i); 90 | } 91 | 92 | export function useWindowSize() { 93 | const [windowSize, setWindowSize] = useState(getWindowSize); 94 | 95 | const handleResize = useCallback(() => { 96 | setWindowSize(getWindowSize()); 97 | }, []); 98 | 99 | useLayoutEffect(() => { 100 | handleResize(); 101 | if (!isMobile()) window.addEventListener('resize', handleResize); 102 | return () => { 103 | if (!isMobile()) window.removeEventListener('resize', handleResize); 104 | } 105 | }, []); 106 | 107 | return windowSize; 108 | } 109 | 110 | export function getElementSize(div: HTMLElement) { 111 | return { 112 | width: div.offsetWidth, 113 | height: div.offsetHeight 114 | } as GraphicsSize; 115 | } 116 | 117 | export function useElementSize(elRef: React.RefObject) { 118 | const [elSize, setElSize] = useState(elRef.current ? getElementSize(elRef.current) : { width: 0, height: 0 } as GraphicsSize); 119 | 120 | const handleResize = () => { 121 | if (elRef.current) { 122 | const size = getElementSize(elRef.current); 123 | setElSize(size); 124 | } 125 | }; 126 | 127 | useEffect(() => { 128 | if (elRef.current) { 129 | handleResize(); 130 | window.addEventListener("resize", handleResize); 131 | return () => { 132 | if (elRef.current) 133 | window.removeEventListener("resize", handleResize); 134 | } 135 | } 136 | return () => { }; 137 | }, [elRef]); 138 | 139 | return elSize; 140 | } 141 | 142 | export function useScrollPosition(divRef: React.RefObject) { 143 | const [scrollPos, setScrollPos] = useState([0, 0]); 144 | 145 | const onScroll = (e: Event) => { 146 | if (divRef.current) 147 | setScrollPos([divRef.current.scrollLeft, divRef.current.scrollTop]); 148 | } 149 | 150 | useEffect(() => { 151 | if (divRef.current) { 152 | divRef.current.addEventListener("scroll", onScroll); 153 | } 154 | return () => { 155 | if (divRef.current) { 156 | divRef.current.removeEventListener("scroll", onScroll); 157 | } 158 | }; 159 | }, []); 160 | 161 | return scrollPos; 162 | } 163 | 164 | export function stringIsValidNumber(n: string) { 165 | const q = n.match(/^[-+]?\d*(\.\d*)?([eE][-+]?\d+)?$/); 166 | 167 | return (q && q.length > 0) || false; 168 | } 169 | 170 | export interface IEnumValue { 171 | name: string; 172 | value: number; 173 | } 174 | 175 | /** creates an array of {value:number, name:string} suitable for menuitem and options array creation */ 176 | export function mapEnum(e: any) { 177 | const res = new Array(); 178 | for (const item in e) { 179 | if (isNaN(Number(item))) { 180 | res.push({ value: e[item], name: item }); 181 | } 182 | } 183 | return res; 184 | } 185 | 186 | function getFieldDataRecurse(obj: any, pathParts: string[], lvl: number = 0): any { 187 | if (lvl === pathParts.length - 1) 188 | return obj[pathParts[lvl]]; 189 | else 190 | return getFieldDataRecurse(obj[pathParts[lvl]], pathParts, lvl + 1); 191 | } 192 | 193 | export function getFieldData(obj: any, path: string) { 194 | const fields = path.split('.'); 195 | return getFieldDataRecurse(obj, fields); 196 | } 197 | 198 | function setFieldDataRecurse(obj: any, pathParts: string[], newValue: any, lvl: number = 0): any { 199 | if (lvl === pathParts.length - 1) 200 | obj[pathParts[lvl]] = newValue; 201 | else 202 | setFieldDataRecurse(obj[pathParts[lvl]], pathParts, newValue, lvl + 1); 203 | } 204 | 205 | export function setFieldData(obj: any, path: string, newValue: any) { 206 | const fields = path.split('.'); 207 | return setFieldDataRecurse(obj, fields, newValue); 208 | } 209 | 210 | /** create path from type. usage pathBuilder()("xxx", "yyy", ...) */ 211 | export function pathBuilder() { 212 | return < 213 | K1 extends keyof T, 214 | K2 extends keyof NonNullable, 215 | K3 extends keyof NonNullable[K2]>, 216 | K4 extends keyof NonNullable[K2]>[K3]>, 217 | K5 extends keyof NonNullable[K2]>[K3]>[K4]>, 218 | > 219 | (p1: K1, p2?: K2, p3?: K3, p4?: K4, p5?: K5) => { 220 | let res = String(p1); 221 | if (p2) { res += "." + p2; } 222 | if (p3) { res += "." + p3; } 223 | if (p4) { res += "." + p4; } 224 | if (p5) { res += "." + p5; } 225 | return res; 226 | }; 227 | } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasApi.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvasSelection } from "./WSCanvasSelection"; 2 | import { WSCanvasCellCoord } from "./WSCanvasCellCoord"; 3 | import { WSCanvasCoord } from "./WSCanvasCoord"; 4 | import { WSCanvasColumnSortInfo } from "./WSCanvasSortDirection"; 5 | import { WSCanvasStates } from "./WSCanvasStates"; 6 | import { WSCanvasSyncFn } from "./WSCanvas"; 7 | 8 | /** react-ws-canvas API 9 | * Glossary: 10 | * - `real cell coordinates` : row,col indexes correspond to dataset 11 | * - `view cell coordinates` : row,col indexes correspond to graphic canvas ( affected by filter and/or sort ) 12 | */ 13 | export class WSCanvasApi { 14 | 15 | /** initiate API block ( clones api.states ) */ 16 | begin: () => void; 17 | 18 | /** finalize API blocm ( stores api.states )*/ 19 | commit: (forceSort?: boolean) => void; 20 | 21 | /** clones dataset into api.ds */ 22 | prepareCellDataset: () => void; 23 | /** change cell value into api.ds */ 24 | setCellData: (cell: WSCanvasCellCoord, value: any) => void; 25 | /** commit api.ds to current bounded data */ 26 | commitCellDataset: () => void; 27 | /** retrieve data of given real cell */ 28 | getCellData: (cell: WSCanvasCellCoord) => any; 29 | 30 | /** check whatever given key isn't a movement key */ 31 | isDirectEditingKey: (e: React.KeyboardEvent) => boolean; 32 | 33 | /** force filter */ 34 | filter: () => void; 35 | 36 | /** force sort */ 37 | sort: () => void; 38 | 39 | /** set selection to focused cell */ 40 | selectFocusedCell: () => void; 41 | 42 | /** remove current selection ( not the content ) */ 43 | clearSelection: () => void; 44 | 45 | /** retrieve current selection ( view coordinates ) */ 46 | getViewSelection: () => WSCanvasSelection; 47 | 48 | /** retrieve current real selection [ not optimized ] */ 49 | getRealSelection: () => WSCanvasSelection; 50 | 51 | /** set current selection ( view cells ) */ 52 | setViewSelection: (viewSelection: WSCanvasSelection) => void; 53 | 54 | /** set current selection ( real cells ) */ 55 | setRealSelection: (realSelection: WSCanvasSelection) => void; 56 | 57 | /** convert given selection ( view cells ) into a real one [ not optimized ] */ 58 | viewSelectionToReal: (viewSelection: WSCanvasSelection) => WSCanvasSelection; 59 | 60 | /** convert given selection ( real cells ) into a view one [ not optimized ] */ 61 | realSelectionToView: (realSelection: WSCanvasSelection) => WSCanvasSelection; 62 | 63 | /** convert client x,y to canvas relative coordinate */ 64 | clientXYToCanvasCoord: (x: number, y: number) => WSCanvasCoord | null; 65 | 66 | /** retrieve real cell x,y,width,height */ 67 | cellToCanvasCoord: (cell: WSCanvasCellCoord) => WSCanvasCoord | null; 68 | 69 | /** coordinates of canvas */ 70 | canvasCoord: () => WSCanvasCoord | null; 71 | 72 | /** convert given coordinate relative to canvas into real cell coordinate */ 73 | canvasCoordToCellCoord: (ccoord: WSCanvasCoord) => WSCanvasCellCoord | null; 74 | 75 | /** set focus to given real cell */ 76 | focusCell: (coord: WSCanvasCellCoord, scrollTo?: boolean, endingCell?: boolean, clearSelection?: boolean) => void; 77 | 78 | /** set scroll to view given real cell */ 79 | scrollTo: (coord: WSCanvasCellCoord) => void; 80 | 81 | /** change column sorting */ 82 | setSorting: (sorting: WSCanvasColumnSortInfo[]) => void; 83 | 84 | /** open custom editor at given real cell */ 85 | openCustomEdit: (cell: WSCanvasCellCoord) => void; 86 | 87 | /** close current custom editor */ 88 | closeCustomEdit: (confirm: boolean) => void; 89 | 90 | /** change custom editor value */ 91 | setCustomEditValue: (val: any) => void; 92 | 93 | /** go to next view cell */ 94 | goToNextCell: () => void; 95 | 96 | /** simulate key press */ 97 | triggerKey: (e: React.KeyboardEvent) => void; 98 | 99 | /** convert view to to real row */ 100 | viewRowToRealRow: (viewRow: number) => number; 101 | 102 | /** convert real row to view row */ 103 | realRowToViewRow: (realRow: number) => number; 104 | 105 | /** convert real cell to view cell */ 106 | realCellToView: (realCell: WSCanvasCellCoord) => WSCanvasCellCoord; 107 | 108 | /** convert view cell to real cell */ 109 | viewCellToReal: (realCell: WSCanvasCellCoord) => WSCanvasCellCoord; 110 | 111 | /** copy current selection to clipboard suitable to paste in spreadsheet */ 112 | copySelectionToClipboard: (sel: WSCanvasSelection) => void; 113 | 114 | /** copy entire worksheet to clipboard suitable to paste in spreadsheet */ 115 | copyWorksheetToClipboard: () => void; 116 | 117 | /** states if current selection cover entire worksheet */ 118 | selectionIsFullWorksheet: () => boolean; 119 | 120 | /** format given Date object accordingly moment format for Date (see props) */ 121 | formatCellDataAsDate: (cellData: any) => string; 122 | 123 | /** format given Date object accordingly moment format for Time (see props) */ 124 | formatCellDataAsTime: (cellData: any) => string; 125 | 126 | /** format given Date object accordingly moment format for DateTime (see props) */ 127 | formatCellDataAsDateTime: (cellData: any) => string; 128 | 129 | /** force paint */ 130 | paint: () => void; 131 | 132 | /** force view reset */ 133 | resetView: (resetSorting?: boolean, resetFilters?: boolean) => void; 134 | 135 | /** testing */ 136 | onSync: (fn: WSCanvasSyncFn) => void; 137 | 138 | /** current states */ 139 | states: WSCanvasStates; 140 | 141 | /** dataset if using prepareCellDataset, setCellData, commitCellDataset */ 142 | ds: any; 143 | 144 | constructor(states: WSCanvasStates) { 145 | this.states = states; 146 | 147 | this.begin = () => { }; 148 | this.commit = () => { }; 149 | this.onSync = () => { }; 150 | 151 | this.prepareCellDataset = () => { }; 152 | this.setCellData = () => { }; 153 | this.commitCellDataset = () => { }; 154 | this.getCellData = () => null; 155 | 156 | this.clearSelection = () => { }; 157 | this.isDirectEditingKey = () => false; 158 | this.filter = () => { }; 159 | this.sort = () => { }; 160 | this.selectFocusedCell = () => { }; 161 | this.getViewSelection = () => new WSCanvasSelection([]); 162 | this.getRealSelection = () => new WSCanvasSelection([]); 163 | this.setViewSelection = () => { }; 164 | this.setRealSelection = () => { }; 165 | this.realSelectionToView = () => new WSCanvasSelection([]); 166 | this.viewSelectionToReal = () => new WSCanvasSelection([]); 167 | this.copySelectionToClipboard = () => { }; 168 | this.copyWorksheetToClipboard = () => { }; 169 | this.selectionIsFullWorksheet = () => false; 170 | 171 | this.clientXYToCanvasCoord = () => null; 172 | this.cellToCanvasCoord = () => null; 173 | this.canvasCoord = () => null; 174 | this.canvasCoordToCellCoord = () => null; 175 | this.focusCell = () => { }; 176 | this.scrollTo = () => { }; 177 | this.setSorting = () => { }; 178 | this.openCustomEdit = () => { }; 179 | this.closeCustomEdit = () => { }; 180 | this.setCustomEditValue = () => { }; 181 | this.goToNextCell = () => { }; 182 | this.triggerKey = () => { }; 183 | this.viewRowToRealRow = () => 0; 184 | this.realRowToViewRow = () => 0; 185 | this.realCellToView = () => new WSCanvasCellCoord(); 186 | this.viewCellToReal = () => new WSCanvasCellCoord(); 187 | this.formatCellDataAsDate = () => ""; 188 | this.formatCellDataAsTime = () => ""; 189 | this.formatCellDataAsDateTime = () => ""; 190 | 191 | this.paint = () => { }; 192 | this.resetView = () => { }; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /example/src/lib/WSCanvasCellCoord.tsx: -------------------------------------------------------------------------------- 1 | export class WSCanvasCellCoord { 2 | private _rowIdx: number; 3 | private _colIdx: number; 4 | private _filterRow: boolean; 5 | 6 | constructor(rowIdx: number = 0, colIdx: number = 0, filterRow: boolean = false) { 7 | this._rowIdx = rowIdx; 8 | this._colIdx = colIdx; 9 | this._filterRow = filterRow; 10 | } 11 | 12 | get row() { return this._rowIdx; } 13 | get col() { return this._colIdx; } 14 | get filterRow() { return this._filterRow; } 15 | 16 | key = () => this.toString(); 17 | 18 | setRow = (newRow: number) => new WSCanvasCellCoord(newRow, this._colIdx); 19 | setCol = (newCol: number) => new WSCanvasCellCoord(this._rowIdx, newCol); 20 | 21 | nextRow = () => new WSCanvasCellCoord(this._rowIdx + 1, this._colIdx); 22 | prevRow = () => new WSCanvasCellCoord(this._rowIdx - 1, this._colIdx); 23 | nextCol = (colsCnt: number, qColHidden: (c: number) => boolean) => { 24 | let _c = this._colIdx + 1; 25 | while (_c < colsCnt && qColHidden(_c))++_c; 26 | return new WSCanvasCellCoord(this._rowIdx, _c); 27 | } 28 | prevCol = (qColHidden: (c: number) => boolean) => { 29 | let _c = this._colIdx - 1; 30 | while (_c > 0 && qColHidden(_c))--_c; 31 | return new WSCanvasCellCoord(this._rowIdx, _c); 32 | } 33 | 34 | lessThan(other: WSCanvasCellCoord) { 35 | return this.row < other.row || (this.row === other.row && this.col < other.col); 36 | } 37 | 38 | greatThan(other: WSCanvasCellCoord) { 39 | return this.row > other.row || (this.row === other.row && this.col > other.col); 40 | } 41 | 42 | equals(other: WSCanvasCellCoord) { 43 | return this.row === other.row && this.col === other.col; 44 | } 45 | 46 | toString() { return "(r:" + this._rowIdx + ", c:" + this._colIdx + ")"; } 47 | } 48 | -------------------------------------------------------------------------------- /example/src/lib/WSCanvasColumn.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvasSortDirection } from "./WSCanvasSortDirection"; 2 | import { WSCanvasCellCoord } from "./WSCanvasCellCoord"; 3 | import { WSCanvasStates } from "./WSCanvasStates"; 4 | import { WSCanvasProps } from "./WSCanvasProps"; 5 | import { CSSProperties } from "react"; 6 | 7 | export type WSCanvasColumnType = "text" | "number" | "boolean" | "custom" | "date" | "time" | "datetime"; 8 | 9 | export enum WSCanvasColumnClickBehavior { 10 | ToggleSort, 11 | Select, 12 | None 13 | } 14 | 15 | export interface WSCanvasSortingRowInfo { 16 | ri: number; 17 | cellData: any; 18 | } 19 | 20 | export interface WSCanvasColumnWithIdx { 21 | colNfo: WSCanvasColumn; 22 | colIdx: number; 23 | } 24 | 25 | /** helper to store column info ; see hints for WSCanvas prop transferring */ 26 | export interface WSCanvasColumn { 27 | /** hint: getCellType={(cell, data) => columns[cell.col].type} */ 28 | type?: WSCanvasColumnType; 29 | 30 | /** hint: getColumnHeader={(col) => columns[col].header} */ 31 | header?: string; 32 | 33 | /** hint: 34 | * dataSource={rows} 35 | * getCellData={(cell) => (rows[cell.row] as any)[columns[cell.col].field]} 36 | * prepareCellDataset={() => rows.slice()} 37 | * commitCellDataset={(q) => setRows(q)} 38 | */ 39 | field: string; 40 | 41 | /** hint: getColumnLessThanOp={(col) => columns[col].lessThan} */ 42 | lessThan?: (a: any, b: any) => boolean; 43 | 44 | textAlign?: (coord: WSCanvasCellCoord, value: any) => CanvasTextAlign | undefined; 45 | 46 | /** hint: colWidth={(col) => columns[col].width || 100} */ 47 | width?: number; 48 | 49 | /** hint: columnInitialSort={WSCanvasColumnToSortInfo(columns)} */ 50 | sortDirection?: WSCanvasSortDirection; 51 | 52 | /** hint: columnInitialSort={WSCanvasColumnToSortInfo(columns)} */ 53 | sortOrder?: number; 54 | 55 | /** hint: getCellTextWrap={(cell, props) => { if (columns[cell.col].wrapText) return columns[cell.col].wrapText; }} 56 | * if customRender active then real element height will retrieved */ 57 | wrapText?: boolean; 58 | 59 | renderTransform?: (row: any, cell: WSCanvasCellCoord, value: any) => any; 60 | 61 | /** if true datasource will be used instead of renderTransform if present */ 62 | filterUseDataSource?: boolean; 63 | 64 | /** custom dataset filter (doesn't consider renderTransform even if filterUseDataSource is set) */ 65 | dsFilter?: (row: any, cell: WSCanvasCellCoord, value: any) => boolean; 66 | 67 | readonly?: boolean | undefined; 68 | 69 | customRender?: ((states: WSCanvasStates, row: any, cell: WSCanvasCellCoord, 70 | containerStyle?: CSSProperties, cellWidth?: number, cellHeight?: number) => JSX.Element | undefined) | undefined, 71 | 72 | customEdit?: ((states: WSCanvasStates, row: any, cell: WSCanvasCellCoord, 73 | containerStyle?: CSSProperties, cellWidth?: number, cellHeight?: number) => JSX.Element | undefined) | undefined, 74 | 75 | hidden?: boolean | undefined; 76 | 77 | /** fired before cell editing done ; can prevent editing done by return false */ 78 | onChanging?: (states: WSCanvasStates, row: any, cell: WSCanvasCellCoord, oldValue: any, newValue: any) => boolean; 79 | 80 | /** fired after cell edited */ 81 | onChanged?: (states: WSCanvasStates, row: any, cell: WSCanvasCellCoord, oldValue: any, newValue: any) => void; 82 | } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasCoord.tsx: -------------------------------------------------------------------------------- 1 | export class WSCanvasCoord { 2 | private _x: number; 3 | private _y: number; 4 | private _width: number; 5 | private _height: number; 6 | 7 | constructor(x: number = 0, y: number = 0, width: number = 0, height: number = 0) { 8 | this._x = x; 9 | this._y = y; 10 | this._width = width; 11 | this._height = height; 12 | } 13 | 14 | get x() { return this._x; } 15 | get y() { return this._y; } 16 | get width() { return this._width; } 17 | get height() { return this._height; } 18 | 19 | equals(other: WSCanvasCoord) { 20 | return this._x === other._x && this._y === other._y && 21 | this._width === other._width && this._height === other._height; 22 | } 23 | 24 | sum(delta: WSCanvasCoord) { 25 | return new WSCanvasCoord(this._x + delta._x, this._y + delta._y); 26 | } 27 | 28 | key = () => this.toString(); 29 | 30 | toString() { return "(x:" + this._x.toFixed(0) + ", y:" + this._y.toFixed(0) + " w:" + this._width.toFixed(0) + ", h:" + this._height.toFixed(0) + ")"; } 31 | } 32 | -------------------------------------------------------------------------------- /example/src/lib/WSCanvasEditMode.tsx: -------------------------------------------------------------------------------- 1 | export enum WSCanvasEditMode { 2 | none, 3 | direct, 4 | F2 5 | } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasFilter.tsx: -------------------------------------------------------------------------------- 1 | export interface WSCanvasFilter { 2 | colIdx: number; 3 | filter: any; 4 | } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasProps.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvasScrollbarMode } from "./WSCanvasScrollbarMode"; 2 | import { WSCanvasSelectMode } from "./WSCanvasSelectionMode"; 3 | import { WSCanvasCellCoord } from "./WSCanvasCellCoord"; 4 | import { WSCanvasColumnType, WSCanvasColumnClickBehavior, WSCanvasColumn } from "./WSCanvasColumn"; 5 | import { WSCanvasApi } from "./WSCanvasApi"; 6 | import { CSSProperties } from "react"; 7 | import { WSCanvasColumnSortInfo } from "./WSCanvasSortDirection"; 8 | import { WSCanvasStates } from "./WSCanvasStates"; 9 | import { WSCanvasXYCellCoord } from "./WSCanvasXYCellCoord"; 10 | 11 | export interface WSCanvasCellDataNfo { 12 | coord: WSCanvasCellCoord; 13 | value: any; 14 | } 15 | 16 | /** see WSCanvasPropsDefault for default values */ 17 | export interface WSCanvasProps { 18 | /** custom identifier */ 19 | id?: string; 20 | /** width 100% [default: false] */ 21 | fullwidth: boolean; 22 | /** width of canvas [default: window.innerWidth] */ 23 | width: number; 24 | /** height of canvas [default: window.innerHeight] */ 25 | height: number; 26 | /** rows datasource (note: when sort changes array order will change accordingly) [default: []] */ 27 | rows: any[]; 28 | /** nr of rows in the grid [default: 0] */ 29 | rowsCount: number; 30 | /** nr of cols in the grid ( or use columns ) [default: 0] */ 31 | colsCount?: number; 32 | /** compact column info [default: undefined] */ 33 | columns?: WSCanvasColumn[]; 34 | /** width of column in the grid ( or use columns ) [default: undefined] */ 35 | colWidth?: (cidx: number) => number; 36 | /** expand column width to fit control width ( if column width sum not already exceed control width ) [default: true] */ 37 | colWidthExpand: boolean; 38 | /** height of rows in the grid [default: DEFAULT_ROW_HEIGHT=30] 39 | * if overriden, when row===null or ridx===-1 should return default height */ 40 | rowHeight: (row: any, ridx: number) => number; 41 | /** nr of frozen rows [default: 0] */ 42 | frozenRowsCount: number; 43 | /** nr of frozen cols [default: 0] */ 44 | frozenColsCount: number; 45 | /** selection mode allow append using ctrl key [default: true] */ 46 | selectionModeMulti: boolean; 47 | /** selection mode row or cell [default: WSCanvasSelectMode.Cell] */ 48 | selectionMode: WSCanvasSelectMode; 49 | /** show focused cell outline [default: false] */ 50 | showFocusedCellOutline: boolean; 51 | /** show row numbers column [default: false] */ 52 | showRowNumber: boolean; 53 | /** highlight row header matching current sel [default: true] */ 54 | highlightRowNumber: boolean; 55 | /** show column headers row [default: false] */ 56 | showColNumber: boolean; 57 | /** highlight column header matching current sel [default: true] */ 58 | highlightColNumber: boolean; 59 | /** behavior for column header click sort or select [default: WSCanvasColumnClickBehavior.ToggleSort] */ 60 | columnClickBehavior: WSCanvasColumnClickBehavior; 61 | /** show column filters row [default: false] */ 62 | showFilter: boolean; 63 | /** whatever select first avail row after filtering [default: false] */ 64 | selectFirstOnFilter: boolean; 65 | /** if set true filter consider datasource instead of renderTransform if present [default: ()=>false]*/ 66 | filterUseDatasource: (cell: WSCanvasCellCoord) => boolean; 67 | /** show or truncate partial columns [default: true] */ 68 | showPartialColumns: boolean; 69 | /** show or truncate partial rows [default: true] */ 70 | showPartialRows: boolean; 71 | /** prevent wheel default window scroll when scroll at top or bottom [default: true] */ 72 | preventWheelOnBounds: boolean; 73 | /** if set new rows goes inserted at given view index [default: undefined] */ 74 | newRowsInsertAtViewIndex?: number; 75 | /** allow to force a global filter ; rows that not satisfy the filter aren't shown */ 76 | globalFilter?: (row: any, ridx:number) => boolean | undefined; 77 | 78 | /** retrieve data from a row */ 79 | rowGetCellData?: (row: any, colIdx: number) => any; 80 | /** allow to transform data before being displayed (useful for enum types); if defined must return input data as is or transformed ( or use columns ) [default: undefined] */ 81 | renderTransform?: (row: any, cell: WSCanvasCellCoord, data: any) => any; 82 | /** retrieve rows dataset copy [default: []] */ 83 | prepareCellDataset: () => any; 84 | /** retrieve rows from dataset [default: (ds) => ds as any[]] */ 85 | cellDatasetGetRows: (ds: any) => any[]; 86 | /** set row cell data [default: {}] */ 87 | rowSetCellData: (row: any, colIdx: number, value: any) => void; 88 | /** set cell dataset state [default: {}] */ 89 | commitCellDataset: (dataset: any) => void; 90 | /** allow to define a custom editor or return undefined to use builtin cell editor [default: undefined] */ 91 | getCellCustomEdit?: ((states: WSCanvasStates, row: any, cell: WSCanvasCellCoord, 92 | containerStyle?: CSSProperties, cellWidth?: number, cellHeight?: number) => JSX.Element) | undefined; 93 | /** header of given col ( or use columns ) [default: undefined] */ 94 | getColumnHeader?: (col: number) => string | undefined; 95 | /** states whatever colum should hidden [default: undefined] */ 96 | getColumnHidden?: (col: number) => boolean; 97 | /** column sort method ( or use columns ) [default: undefined] */ 98 | getColumnLessThanOp?: (col: number) => ((a: any, b: any) => boolean) | undefined; 99 | /** specify type of a cell ( or use columns) [default: undefined] */ 100 | getCellType?: (row: any, coord: WSCanvasCellCoord, value: any) => WSCanvasColumnType | undefined; 101 | /** specify cell editor inhibit ( or use columns ) [default: undefined] */ 102 | isCellReadonly?: (row: any, coord: WSCanvasCellCoord) => boolean | undefined; 103 | /** specify predefined column sort ( WSCanvasColumn array helper or use columns ) [default: undefined] */ 104 | columnInitialSort?: WSCanvasColumnSortInfo[] | undefined; 105 | /** specify text align of a cell ( or use columns ) [default: undefined] */ 106 | getCellTextAlign?: (row: any, coord: WSCanvasCellCoord, value: any) => CanvasTextAlign | undefined; 107 | 108 | /** individual cell background customization [default: undefined] */ 109 | getCellBackgroundColor?: (row: any, coord: WSCanvasCellCoord, props: WSCanvasProps) => string | undefined; 110 | /** cell background [default: "white"] */ 111 | sheetBackgroundColor: string; 112 | /** grid lines color [default: "#c0c0c0"] */ 113 | gridLinesColor: string; 114 | /** color of frozen row/cols separator line [default: "black"] */ 115 | frozenCellGridLinesColor: string; 116 | /** color or focused cell outline [default: "black"] */ 117 | focusedCellBorderColor: string; 118 | 119 | /** background color of selected cells [default: "#f9d4c7"] */ 120 | selectionBackgroundColor: string; 121 | /** border color of selection [default: "#e95420"] */ 122 | selectionBorderColor: string; 123 | /** color of row/col numbers [default: "white"] */ 124 | selectedHeaderTextColor: string; 125 | /** background of row/col numbers [default: "#e95420"] */ 126 | selectedHeaderBackgroundColor: string; 127 | 128 | /** moment formatting for date type cells [default: "L"] */ 129 | dateCellMomentFormat: string; 130 | /** moment formatting for time type cells [default: "LT"] */ 131 | timeCellMomentFormat: string; 132 | /** moment formatting for datetime type cells (ex. "L LTS" to include seconds) [default: "L LT"] */ 133 | dateTimeCellMomentFormat: string; 134 | /** margin of text inside cells [default: 2] */ 135 | textMargin: number; 136 | /** individual cell text wrap ( or use columns ) [default: undefined] */ 137 | getCellTextWrap?: (row: any, coord: WSCanvasCellCoord, props: WSCanvasProps) => boolean | undefined; 138 | /** individual cell font customization [default: undefined] */ 139 | getCellFont?: (row: any, coord: WSCanvasCellCoord, props: WSCanvasProps) => string | undefined; 140 | /** font of cells text [default: "12px Liberation Sans"] */ 141 | font: string; 142 | /** individual cell text color customization [default: undefined] */ 143 | getCellTextColor?: (row: any, coord: WSCanvasCellCoord, props: WSCanvasProps) => string | undefined; 144 | /** color of cell text [default: "black"] */ 145 | cellTextColor: string; 146 | /** font of row/col nunbers [default: "16px Liberation Sans"] */ 147 | headerFont: string; 148 | /** default cell cursor [default: "default"] */ 149 | cellCursor: string; 150 | /** default non cell cursor [default: "default"] */ 151 | outsideCellCursor: string; 152 | /** row hover (ex. "rgba(127,127,127, 0.1)") [default: undefined] */ 153 | rowHoverColor: (row: any, ridx: number) => string | undefined; 154 | 155 | /** filter apply debounce (ms) [default: 500] */ 156 | filterDebounceMs: number; 157 | /** filter edit cell margin [default: 3] */ 158 | filterTextMargin: number; 159 | /** filter match ignore case [default: true] */ 160 | filterIgnoreCase: boolean; 161 | /** filter cell background color [default: "yellow"] */ 162 | filterBackground: string; 163 | /** if autoselect all text when click on filter */ 164 | filterAutoSelectAll: boolean; 165 | 166 | /** ms from last column change to recompute row height [default: 0] */ 167 | recomputeRowHeightDebounceFilterMs: number; 168 | /** width of row numbers col [default: 80] */ 169 | rowNumberColWidth: number; 170 | /** height of column numbers row [default: DEFAULT_ROW_HEIGHT=30] */ 171 | colNumberRowHeight: number; 172 | /** background of row/col number cells [default: "#f5f6f7"] */ 173 | cellNumberBackgroundColor: string; 174 | 175 | /** mode of vertical scrollbar ( auto, on, off ) [default: WSCanvasScrollbarMode.auto] */ 176 | verticalScrollbarMode: WSCanvasScrollbarMode; 177 | /** mode of horizontal scrollbar ( auto, on, off ) [default: WSCanvasScrollbarMode.auto] */ 178 | horizontalScrollbarMode: WSCanvasScrollbarMode; 179 | /** thickness of scrollbar [default: 10] */ 180 | scrollBarThk: number; 181 | /** min length of scrollbar handle [default: 20] */ 182 | minScrollHandleLen: number; 183 | /** scrollbar handle color [default: "#878787"] */ 184 | scrollBarColor: string; 185 | /** scrollbar handle actived color [default: "red"] */ 186 | clickedScrollBarColor: string; 187 | 188 | /** div container custom styles [default: undefined] */ 189 | containerStyle: CSSProperties | undefined; 190 | /** canvas custom styles ( margin and padding will overriden to 0; use containerStyle for these ) [default: undefined] */ 191 | canvasStyle: CSSProperties | undefined; 192 | 193 | /** enable debug div ( for dev purpose ) [default: false] */ 194 | debug: boolean; 195 | /** div where to place debug [default: undefined] */ 196 | dbgDiv: React.RefObject | undefined; 197 | 198 | /** receive api */ 199 | onApi?: (api: WSCanvasApi) => void, 200 | 201 | onStateChanged?: (states: WSCanvasStates) => void; 202 | 203 | onMouseOverCell?: (states: WSCanvasStates, nfo: WSCanvasXYCellCoord | null) => void; 204 | 205 | /** fired when custom edit opens */ 206 | onCustomEdit?: (states: WSCanvasStates, cell: WSCanvasCellCoord) => void; 207 | 208 | onPreviewKeyDown?: (states: WSCanvasStates, e: React.KeyboardEvent) => void; 209 | onKeyDown?: (states: WSCanvasStates, e: React.KeyboardEvent) => void; 210 | 211 | /** cell click ( row=-1 if column header click ; col=-1 if row header click ) */ 212 | onPreviewMouseDown?: (states: WSCanvasStates, e: React.MouseEvent, cell: WSCanvasCellCoord | null) => void; 213 | onMouseDown?: (states: WSCanvasStates, e: React.MouseEvent, cell: WSCanvasCellCoord | null) => void; 214 | 215 | onPreviewMouseUp?: (states: WSCanvasStates, e: React.MouseEvent) => void; 216 | onMouseUp?: (states: WSCanvasStates, e: React.MouseEvent) => void; 217 | 218 | onPreviewMouseMove?: (states: WSCanvasStates, e: React.MouseEvent) => void; 219 | onMouseMove?: (states: WSCanvasStates, e: React.MouseEvent) => void; 220 | 221 | onPreviewMouseDoubleClick?: (states: WSCanvasStates, e: React.MouseEvent, cell: WSCanvasCellCoord | null) => void; 222 | onMouseDoubleClick?: (states: WSCanvasStates, e: React.MouseEvent, cell: WSCanvasCellCoord | null) => void; 223 | 224 | onPreviewMouseWheel?: (states: WSCanvasStates, e: WheelEvent) => void; 225 | onMouseWheel?: (states: WSCanvasStates, e: WheelEvent) => void; 226 | 227 | onContextMenu?: (states: WSCanvasStates, e: React.MouseEvent, cell: WSCanvasCellCoord | null) => void; 228 | 229 | /** fired before cell editing done ; can prevent editing done by return false */ 230 | onCellEditing?: (states: WSCanvasStates, row: any, cell: WSCanvasCellCoord, oldValue: any, newValue:any) => boolean; 231 | 232 | /** fired after cell edited */ 233 | onCellEdited?: (states: WSCanvasStates, row: any, cell: WSCanvasCellCoord, oldValue: any, newValue:any) => void; 234 | } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasPropsDefault.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvasProps } from "./WSCanvasProps"; 2 | import { WSCanvasScrollbarMode } from "./WSCanvasScrollbarMode"; 3 | import { WSCanvasSelectMode } from "./WSCanvasSelectionMode"; 4 | import { WSCanvasApi } from "./WSCanvasApi"; 5 | import { WSCanvasColumnClickBehavior } from "./WSCanvasColumn"; 6 | import { WSCanvasCellCoord } from "./WSCanvasCellCoord"; 7 | 8 | export const DEFAULT_ROW_HEIGHT = 30; 9 | export const DEFAULT_COL_WIDTH = 120; 10 | 11 | export const WSCanvasPropsDefault = () => { 12 | return { 13 | id: undefined, 14 | onApi: undefined, 15 | handlers: undefined, 16 | 17 | fullwidth: false, 18 | width: window.innerWidth, 19 | height: window.innerHeight, 20 | rows: [], 21 | rowsCount: 0, 22 | colsCount: 0, 23 | columns: undefined, 24 | colWidth: undefined, 25 | colWidthExpand: true, 26 | rowHeight: (row, ridx) => DEFAULT_ROW_HEIGHT, 27 | frozenRowsCount: 0, 28 | frozenColsCount: 0, 29 | selectionModeMulti: true, 30 | selectionMode: WSCanvasSelectMode.Cell, 31 | showFocusedCellOutline: false, 32 | showRowNumber: false, 33 | highlightRowNumber: true, 34 | showColNumber: false, 35 | highlightColNumber: true, 36 | columnClickBehavior: WSCanvasColumnClickBehavior.ToggleSort, 37 | showFilter: false, 38 | selectFirstOnFilter: false, 39 | filterUseDatasource: () => false, 40 | showPartialColumns: true, 41 | showPartialRows: true, 42 | preventWheelOnBounds: true, 43 | newRowsInsertAtViewIndex: undefined, 44 | globalFilter: undefined, 45 | 46 | rowGetCellData: undefined, 47 | renderTransform: undefined, 48 | prepareCellDataset: () => [], 49 | cellDatasetGetRows: (ds) => ds as any[], 50 | rowSetCellData: (row: any, colIdx: number, value: any) => { }, 51 | commitCellDataset: (dataset: any) => { }, 52 | getCellCustomEdit: undefined, 53 | getColumnHeader: undefined, 54 | getColumnHidden: undefined, 55 | getColumnLessThanOp: undefined, 56 | getCellType: undefined, 57 | isCellReadonly: undefined, 58 | columnInitialSort: undefined, 59 | getCellTextAlign: undefined, 60 | 61 | getCellBackgroundColor: undefined, 62 | sheetBackgroundColor: "white", 63 | gridLinesColor: "#c0c0c0", 64 | frozenCellGridLinesColor: "black", 65 | focusedCellBorderColor: "black", 66 | 67 | selectionBackgroundColor: "#f9d4c7", 68 | selectionBorderColor: "#e95420", 69 | selectedHeaderTextColor: "white", 70 | selectedHeaderBackgroundColor: "#e95420", 71 | 72 | dateCellMomentFormat: "L", 73 | timeCellMomentFormat: "LT", 74 | dateTimeCellMomentFormat: "L LT", 75 | textMargin: 2, 76 | getCellTextWrap: undefined, 77 | getCellFont: undefined, 78 | font: "12px Liberation Sans", 79 | getCellTextColor: undefined, 80 | cellTextColor: "black", 81 | headerFont: "16px Liberation Sans", 82 | cellCursor: "default", 83 | outsideCellCursor: "default", 84 | rowHoverColor: (row, ridx) => undefined, // "rgba(127,127,127, 0.1)", 85 | 86 | filterDebounceMs: 500, 87 | filterTextMargin: 3, 88 | filterIgnoreCase: true, 89 | filterBackground: "yellow", 90 | filterAutoSelectAll: false, 91 | 92 | recomputeRowHeightDebounceFilterMs: 0, 93 | rowNumberColWidth: 80, 94 | colNumberRowHeight: DEFAULT_ROW_HEIGHT, 95 | cellNumberBackgroundColor: "#f5f6f7", 96 | 97 | verticalScrollbarMode: WSCanvasScrollbarMode.auto, 98 | horizontalScrollbarMode: WSCanvasScrollbarMode.auto, 99 | scrollBarThk: 10, 100 | minScrollHandleLen: 20, 101 | scrollBarColor: "#878787", 102 | clickedScrollBarColor: "red", 103 | 104 | containerStyle: undefined, 105 | canvasStyle: undefined, 106 | 107 | debug: false, 108 | dbgDiv: undefined, 109 | } as WSCanvasProps; 110 | } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasRect.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvasCoord } from "./WSCanvasCoord"; 2 | 3 | export enum WSCanvasRectMode { twoPoints, pointAndSize }; 4 | 5 | export class WSCanvasRect { 6 | private _leftTop: WSCanvasCoord; 7 | private _rightBottom: WSCanvasCoord; 8 | 9 | constructor( 10 | p1: WSCanvasCoord = new WSCanvasCoord(), 11 | p2: WSCanvasCoord = new WSCanvasCoord(), 12 | mode: WSCanvasRectMode = WSCanvasRectMode.twoPoints) { 13 | 14 | if (mode === WSCanvasRectMode.twoPoints) { 15 | this._leftTop = new WSCanvasCoord(Math.min(p1.x, p2.x), Math.min(p1.y, p2.y)); 16 | this._rightBottom = new WSCanvasCoord(Math.max(p1.x, p2.x), Math.max(p1.y, p2.y)); 17 | } else { 18 | this._leftTop = new WSCanvasCoord(p1.x, p1.y); 19 | this._rightBottom = new WSCanvasCoord(p1.x + p2.x, p1.y + p2.y); 20 | } 21 | } 22 | 23 | equals(other:WSCanvasRect) { 24 | return this._leftTop.equals(other._leftTop) && this._rightBottom.equals(other._rightBottom); 25 | } 26 | 27 | get leftTop() { return this._leftTop; } 28 | get rightBottom() { return this._rightBottom; } 29 | 30 | get width() { return this._rightBottom.x - this._leftTop.x + 1; } 31 | get height() { return this._rightBottom.y - this._leftTop.y + 1; } 32 | 33 | contains(coord: WSCanvasCoord, tolerance: number = 0) { 34 | return (coord.x >= this._leftTop.x - tolerance) && (coord.y >= this._leftTop.y - tolerance) && 35 | (coord.x <= this._rightBottom.x + tolerance) && (coord.y <= this._rightBottom.y + tolerance); 36 | } 37 | 38 | key = () => this._leftTop + "_" + this._rightBottom; 39 | 40 | toString() { return "(" + this.leftTop + "," + this.rightBottom + ")"; } 41 | } 42 | -------------------------------------------------------------------------------- /example/src/lib/WSCanvasScrollbarMode.tsx: -------------------------------------------------------------------------------- 1 | export enum WSCanvasScrollbarMode { 2 | auto, 3 | on, 4 | off 5 | } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasSelection.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvasSelectionRange as WSCanvasSelectionRange } from "./WSCanvasSelectionRange"; 2 | import { WSCanvasCellCoord } from "./WSCanvasCellCoord"; 3 | import { WSCanvasSelectMode } from "./WSCanvasSelectionMode"; 4 | 5 | export class WSCanvasSelection { 6 | 7 | private _ranges: WSCanvasSelectionRange[]; 8 | 9 | constructor(ranges: WSCanvasSelectionRange[]) { 10 | this._ranges = ranges; 11 | } 12 | 13 | get ranges() { return this._ranges; } 14 | 15 | get empty() { return this._ranges.length === 0; } 16 | 17 | get bounds() { 18 | if (this._ranges.length === 0) 19 | return null; 20 | else { 21 | let res = this._ranges[0].bounds; 22 | for (let i = 1; i < this._ranges.length; ++i) { 23 | const b = this._ranges[i].bounds; 24 | res = res.union(b); 25 | } 26 | return res; 27 | } 28 | } 29 | 30 | /** return copy of this */ 31 | dup(): WSCanvasSelection { 32 | return new WSCanvasSelection(this._ranges.map((r) => r.dup())); 33 | } 34 | 35 | add(cell: WSCanvasCellCoord) { 36 | const res = this.dup(); 37 | 38 | const newRange = new WSCanvasSelectionRange(cell, cell); 39 | res.ranges.push(newRange); 40 | 41 | return res; 42 | } 43 | 44 | /** create a copy of this with last range extends to given cell */ 45 | extendsTo(cell: WSCanvasCellCoord) { 46 | const res = this.dup(); 47 | 48 | const lastrng = res.ranges[res.ranges.length - 1]; 49 | res.ranges.splice(res.ranges.length - 1, 1); 50 | 51 | const newRng = new WSCanvasSelectionRange(lastrng ? lastrng.from : cell, cell); 52 | res.ranges.push(newRng); 53 | 54 | return res; 55 | } 56 | 57 | clearSelection() { 58 | this._ranges = []; 59 | } 60 | 61 | setSelection(range: WSCanvasSelectionRange) { 62 | this._ranges = [range]; 63 | } 64 | 65 | containsCell(cell: WSCanvasCellCoord, selectionMode: WSCanvasSelectMode): boolean { 66 | return this._ranges.find((w) => w.contains(cell, selectionMode === WSCanvasSelectMode.Row)) !== undefined; 67 | } 68 | 69 | /** LOOP: let rngCells = rng.cells(); let cell = rngCells.next(); while (!cell.done) { ...; cell = rngCells.next(); } */ 70 | *cells() { 71 | for (let rngIdx = 0; rngIdx < this._ranges.length; ++rngIdx) { 72 | const rng = this._ranges[rngIdx]; 73 | let rngCells = rng.cells(); 74 | let cell = rngCells.next(); 75 | while (!cell.done) { 76 | yield cell.value; 77 | cell = rngCells.next(); 78 | } 79 | } 80 | } 81 | 82 | rowIdxs() { 83 | let res: Set = new Set(); 84 | 85 | for (let rngIdx = 0; rngIdx < this._ranges.length; ++rngIdx) { 86 | const rng = this._ranges[rngIdx]; 87 | let rngCells = rng.cells(); 88 | let cell = rngCells.next(); 89 | while (!cell.done) { 90 | res.add(cell.value.row); 91 | cell = rngCells.next(); 92 | } 93 | } 94 | 95 | return res; 96 | } 97 | 98 | colIdxs() { 99 | let res: Set = new Set(); 100 | 101 | for (let rngIdx = 0; rngIdx < this._ranges.length; ++rngIdx) { 102 | const rng = this._ranges[rngIdx]; 103 | let rngCells = rng.cells(); 104 | let cell = rngCells.next(); 105 | while (!cell.done) { 106 | res.add(cell.value.col); 107 | cell = rngCells.next(); 108 | } 109 | } 110 | 111 | return res; 112 | } 113 | 114 | toString() { 115 | let str = ""; 116 | for (let i = 0; i < this.ranges.length; ++i) { 117 | if (i > 0) str += " ; " 118 | str += this.ranges[i].toString(); 119 | } 120 | 121 | return str; 122 | } 123 | 124 | } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasSelectionBounds.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvasSelectionRange } from "./WSCanvasSelectionRange"; 2 | import { WSCanvasCellCoord } from "./WSCanvasCellCoord"; 3 | 4 | export class WSEditorSelectionBounds { 5 | private _minRowIdx: number; 6 | private _minColIdx: number; 7 | private _maxRowIdx: number; 8 | private _maxColIdx: number; 9 | 10 | get minRowIdx() { return this._minRowIdx; } 11 | get minColIdx() { return this._minColIdx; } 12 | get maxRowIdx() { return this._maxRowIdx; } 13 | get maxColIdx() { return this._maxColIdx; } 14 | get size() { 15 | return (this._maxColIdx - this.minColIdx + 1) * (this._maxRowIdx - this._minRowIdx + 1); 16 | } 17 | 18 | constructor(rng: WSCanvasSelectionRange) { 19 | this._minRowIdx = rng.from.row < rng.to.row ? rng.from.row : rng.to.row; 20 | this._minColIdx = rng.from.col < rng.to.col ? rng.from.col : rng.to.col; 21 | this._maxRowIdx = rng.from.row > rng.to.row ? rng.from.row : rng.to.row; 22 | this._maxColIdx = rng.from.col > rng.to.col ? rng.from.col : rng.to.col; 23 | } 24 | 25 | /** return new bound that extends this one to given other */ 26 | union(other: WSEditorSelectionBounds) { 27 | return new WSEditorSelectionBounds( 28 | new WSCanvasSelectionRange( 29 | new WSCanvasCellCoord( 30 | this._minRowIdx < other._minRowIdx ? this._minRowIdx : other._minRowIdx, 31 | this._minColIdx > other._minColIdx ? this._minColIdx : other._minColIdx), 32 | new WSCanvasCellCoord( 33 | this._maxRowIdx > other._maxRowIdx ? this._maxRowIdx : other._maxRowIdx, 34 | this._maxColIdx > other._maxColIdx ? this._maxColIdx : other._maxColIdx))); 35 | } 36 | 37 | contains(cell: WSCanvasCellCoord) { 38 | return cell.row >= this._minRowIdx && cell.row <= this._maxRowIdx && 39 | cell.col >= this._minColIdx && cell.col <= this._maxColIdx; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/src/lib/WSCanvasSelectionMode.tsx: -------------------------------------------------------------------------------- 1 | export enum WSCanvasSelectMode { Cell, Row } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasSelectionRange.tsx: -------------------------------------------------------------------------------- 1 | import { WSEditorSelectionBounds } from "./WSCanvasSelectionBounds"; 2 | import { WSCanvasCellCoord } from "./WSCanvasCellCoord"; 3 | 4 | export class WSCanvasSelectionRange { 5 | private _from: WSCanvasCellCoord; 6 | private _to: WSCanvasCellCoord; 7 | 8 | get from() { return this._from; } 9 | get to() { return this._to; } 10 | 11 | /** compute this range bounds */ 12 | get bounds(): WSEditorSelectionBounds { return new WSEditorSelectionBounds(this); } 13 | 14 | constructor(from: WSCanvasCellCoord, to?: WSCanvasCellCoord) { 15 | this._from = from; 16 | if (to === undefined) 17 | this._to = from; 18 | else 19 | this._to = to; 20 | } 21 | 22 | /** returns copy of this */ 23 | dup() { 24 | return new WSCanvasSelectionRange(this.from, this.to); 25 | } 26 | 27 | contains(other: WSCanvasCellCoord, rowMode: boolean = false) { 28 | if (rowMode === true) { 29 | let minRow = this._from.row; 30 | let maxRow = this._to.row; 31 | if (maxRow < minRow) { 32 | let x = minRow; 33 | minRow = maxRow; 34 | maxRow = x; 35 | } 36 | return minRow <= other.row && other.row <= maxRow; 37 | } 38 | else 39 | return this.bounds.contains(other); 40 | } 41 | 42 | toString() { 43 | if (this._from.equals(this._to)) 44 | return "(" + this._from.toString() + ")"; 45 | else 46 | return "(" + this.from.toString() + ")-(" + this.to.toString() + ")"; 47 | } 48 | 49 | /** LOOP: let rngCells = rng.cells(); let cell = rngCells.next(); while (!cell.done) { ...; cell = rngCells.next(); } */ 50 | *cells() { 51 | const bound = this.bounds; 52 | 53 | for (let ri = bound.minRowIdx; ri <= bound.maxRowIdx; ++ri) { 54 | for (let ci = bound.minColIdx; ci <= bound.maxColIdx; ++ci) { 55 | yield new WSCanvasCellCoord(ri, ci); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /example/src/lib/WSCanvasSortDirection.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvasColumn } from "./WSCanvasColumn"; 2 | 3 | export interface WSCanvasColumnSortInfo { 4 | columnIndex: number; 5 | sortOrder: number; 6 | sortDirection: WSCanvasSortDirection; 7 | } 8 | 9 | export const WSCanvasColumnToSortInfo = (columns: WSCanvasColumn[]) => 10 | columns.map((c, idx) => { 11 | return { 12 | columnIndex: idx, 13 | sortDirection: c.sortDirection, 14 | sortOrder: c.sortOrder 15 | } as WSCanvasColumnSortInfo 16 | }); 17 | 18 | export enum WSCanvasSortDirection { 19 | None, 20 | Ascending, 21 | Descending 22 | } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasState.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvasEditMode } from "./WSCanvasEditMode"; 2 | import { WSCanvasSelection } from "./WSCanvasSelection"; 3 | import { WSCanvasCellCoord } from "./WSCanvasCellCoord"; 4 | import { WSCanvasRect } from "./WSCanvasRect"; 5 | import { WSCanvasCoord } from "./WSCanvasCoord"; 6 | import { WSCanvasColumnSortInfo } from "./WSCanvasSortDirection"; 7 | import * as _ from 'lodash'; 8 | import { WSCanvasFilter } from "./WSCanvasFilter"; 9 | import { GraphicsSize } from "./Utils"; 10 | 11 | export class WSCanvasState { 12 | constructor() { 13 | this.colsCountBackup = 0; 14 | this.filteredSortedRowsCount = 0; 15 | this.widthBackup = 0; 16 | this.heightBackup = 0; 17 | 18 | this.viewRowsCount = 0; 19 | this.viewColsCount = 0; 20 | 21 | this.selectedRow = undefined; 22 | this.selectedRowsCount = 0; 23 | 24 | this.viewScrollOffset = new WSCanvasCellCoord(); 25 | this.scrollOffsetStart = new WSCanvasCellCoord(); 26 | this.tableCellsBBox = new WSCanvasRect(); 27 | 28 | this.touchStartTime = 0; 29 | this.touchStart = [0, 0]; 30 | this.touchCur = [0, 0]; 31 | 32 | this.lastPartialColScrolled = -1; 33 | this.focusedCell = new WSCanvasCellCoord(); 34 | this.focusedCellSelectFollow = false; 35 | this.scrollToWhenAvail = null; 36 | this.focusedFilterColIdx = -1; 37 | this.filters = []; 38 | this.filtersTrack = ""; 39 | this.hoveredViewRow = -2; 40 | 41 | this.editMode = WSCanvasEditMode.none; 42 | 43 | this.customEditCell = null; 44 | this.customEditOrigValue = null; 45 | this.customEditValue = null; 46 | this.columnWidthOverride = new Map(); 47 | this.columnWidthOverrideTrack = ""; 48 | this.resizingCol = -2; 49 | this.resizingColStartNfo = [-2, 0]; 50 | this.colWidthExpanded = 0; 51 | 52 | this.viewSelection = new WSCanvasSelection([]); 53 | this.columnsSort = []; 54 | this.cursorOverCell = false; 55 | 56 | this.verticalScrollBarRect = null; 57 | this.verticalScrollHandleRect = null; 58 | this.verticalScrollClickStartCoord = null; 59 | this.verticalScrollClickStartFactor = 0; 60 | this.horizontalScrollBarRect = null; 61 | this.horizontalScrollHandleRect = null; 62 | this.horizontalScrollClickStartCoord = null; 63 | this.horizontalScrollClickStartFactor = 0; 64 | 65 | this.paintcnt = 0; 66 | this.debugNfo = ""; 67 | this.initialized = false; 68 | this.rowsCountBackup = 0; 69 | } 70 | 71 | colsCountBackup: number; 72 | filteredSortedRowsCount: number; 73 | widthBackup: number; 74 | heightBackup: number; 75 | 76 | viewRowsCount: number; 77 | viewColsCount: number; 78 | 79 | /** selected row ( if multiple rows, this is the first ) */ 80 | selectedRow: any; 81 | selectedRowsCount: number; 82 | 83 | viewScrollOffset: WSCanvasCellCoord; 84 | scrollOffsetStart: WSCanvasCellCoord; 85 | tableCellsBBox: WSCanvasRect; 86 | 87 | touchStartTime: number; 88 | touchStart: number[]; 89 | touchCur: number[]; 90 | 91 | lastPartialColScrolled: number; 92 | focusedCell: WSCanvasCellCoord; 93 | focusedCellSelectFollow: boolean; 94 | scrollToWhenAvail: WSCanvasCellCoord | null; 95 | focusedFilterColIdx: number; 96 | filters: WSCanvasFilter[]; 97 | /** json serialization of filters to work with debounce */ 98 | filtersTrack: string; 99 | hoveredViewRow: number; 100 | 101 | editMode: WSCanvasEditMode; 102 | 103 | customEditCell: WSCanvasCellCoord | null; 104 | customEditOrigValue: any; 105 | customEditValue: any; 106 | columnWidthOverride: Map; 107 | /** json serialization of columnWidthOverride to work with debounce */ 108 | columnWidthOverrideTrack: string; 109 | resizingCol: number; 110 | /** x,width */ 111 | resizingColStartNfo: number[]; 112 | colWidthExpanded: number; 113 | 114 | /** view cell selection */ 115 | viewSelection: WSCanvasSelection; 116 | columnsSort: WSCanvasColumnSortInfo[]; 117 | /** false when cursor on canvas out of cells such padding space and scrollbars */ 118 | cursorOverCell: boolean; 119 | 120 | verticalScrollBarRect: WSCanvasRect | null; 121 | verticalScrollHandleRect: WSCanvasRect | null; 122 | verticalScrollClickStartCoord: WSCanvasCoord | null; 123 | verticalScrollClickStartFactor: number; 124 | horizontalScrollBarRect: WSCanvasRect | null; 125 | horizontalScrollHandleRect: WSCanvasRect | null; 126 | horizontalScrollClickStartCoord: WSCanvasCoord | null; 127 | horizontalScrollClickStartFactor: number; 128 | 129 | paintcnt: number; 130 | debugNfo: string; 131 | initialized: boolean; 132 | rowsCountBackup: number; 133 | 134 | dup() { 135 | const q = _.cloneDeep(this) as WSCanvasState; 136 | return q; 137 | } 138 | } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasStates.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvasState } from "./WSCanvasState"; 2 | import { ViewMap } from "./WSCanvas"; 3 | import { WSCanvasProps } from "./WSCanvasProps"; 4 | 5 | export interface WSCanvasStates { 6 | props: WSCanvasProps; 7 | state: WSCanvasState; 8 | vm: ViewMap | null; 9 | overrideRowHeight: number[] | null; 10 | } -------------------------------------------------------------------------------- /example/src/lib/WSCanvasXYCellCoord.tsx: -------------------------------------------------------------------------------- 1 | import { WSCanvasCellCoord } from "./WSCanvasCellCoord"; 2 | 3 | export interface WSCanvasXYCellCoord { 4 | /** client coord */ 5 | xy: number[]; 6 | cell: WSCanvasCellCoord; 7 | } -------------------------------------------------------------------------------- /example/src/lib/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Utils'; 2 | export * from './WSCanvas'; 3 | export * from './WSCanvasApi'; 4 | export * from './WSCanvasCellCoord'; 5 | export * from './WSCanvasColumn'; 6 | export * from './WSCanvasCoord'; 7 | export * from './WSCanvasEditMode'; 8 | export * from './WSCanvasFilter'; 9 | export * from './WSCanvasProps'; 10 | export * from './WSCanvasPropsDefault'; 11 | export * from './WSCanvasRect'; 12 | export * from './WSCanvasScrollbarMode'; 13 | export * from './WSCanvasSelection'; 14 | export * from './WSCanvasSelectionBounds'; 15 | export * from './WSCanvasSelectionMode'; 16 | export * from './WSCanvasSelectionRange'; 17 | export * from './WSCanvasSortDirection'; 18 | export * from './WSCanvasState'; 19 | export * from './WSCanvasStates'; 20 | export * from './WSCanvasXYCellCoord'; 21 | -------------------------------------------------------------------------------- /example/src/quickstart/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import * as serviceWorker from '../serviceWorker'; 5 | import AppQuickStart from '../App.quickstart'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /example/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "incremental": true, 19 | "isolatedModules": true, 20 | "jsx": "react", 21 | "downlevelIteration": true, 22 | "noEmit": true 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /lib/local-publish: -------------------------------------------------------------------------------- 1 | yarn build && yalc publish 2 | -------------------------------------------------------------------------------- /lib/minor-and-publish: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exdir="$(dirname `readlink -f "$0"`)" 4 | 5 | cd "$exdir" 6 | yarn version --minor 7 | 8 | cp -f "$exdir"/../README.md "$exdir" 9 | rm -fr "$exdir"/dist 10 | yarn build 11 | yarn publish 12 | 13 | rm -f "$exdir"/README.md 14 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ws-canvas", 3 | "version": "0.23.6-0", 4 | "description": "Spreadsheet like react canvas datagrid optimized for performance built entirely typescript and react functional components with react hooks.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.es.js", 7 | "author": { 8 | "name": "Lorenzo Delana", 9 | "email": "lorenzo.delana@gmail.com", 10 | "url": "https://github.com/devel0?tab=repositories" 11 | }, 12 | "repository": "https://github.com/devel0/react-ws-canvas", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@types/jest": "24.0.21", 16 | "@types/lodash": "^4.14.146", 17 | "@types/moment": "^2.13.0", 18 | "@types/node": "12.12.5", 19 | "@types/react": "^16.9.11", 20 | "@types/react-dom": "16.9.3", 21 | "copy-to-clipboard": "^3.2.0", 22 | "lodash": "^4.17.15", 23 | "moment": "^2.24.0", 24 | "react": "^16.11.0", 25 | "react-dom": "^16.11.0", 26 | "react-scripts": "3.2.0", 27 | "rollup-plugin-internal": "^1.0.4", 28 | "typescript": "3.6.4" 29 | }, 30 | "scripts": { 31 | "build": "rollup -c" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "peerDependencies": { 37 | "lodash": "^4.17.15", 38 | "moment": "^2.24.0", 39 | "react": "^16.11.0", 40 | "react-dom": "^16.11.0" 41 | }, 42 | "devDependencies": { 43 | "@svgr/rollup": "^4.3.3", 44 | "rollup": "^1.27.0", 45 | "rollup-plugin-babel": "^4.3.3", 46 | "rollup-plugin-commonjs": "^10.1.0", 47 | "rollup-plugin-node-resolve": "^5.2.0", 48 | "rollup-plugin-peer-deps-external": "^2.2.0", 49 | "rollup-plugin-postcss": "^2.0.3", 50 | "rollup-plugin-typescript2": "^0.25.2", 51 | "rollup-plugin-url": "^3.0.0" 52 | }, 53 | "files": [ 54 | "dist" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /lib/prepatch-and-publish: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exdir="$(dirname `readlink -f "$0"`)" 4 | 5 | cd "$exdir" 6 | yarn version --prepatch 7 | 8 | cp -f "$exdir"/../README.md "$exdir" 9 | rm -fr "$exdir"/dist 10 | yarn build 11 | yarn publish 12 | 13 | rm -f "$exdir"/README.md 14 | -------------------------------------------------------------------------------- /lib/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import external from 'rollup-plugin-peer-deps-external'; 4 | // import postcss from 'rollup-plugin-postcss-modules' 5 | import postcss from 'rollup-plugin-postcss'; 6 | import resolve from 'rollup-plugin-node-resolve'; 7 | import url from 'rollup-plugin-url'; 8 | import svgr from '@svgr/rollup'; 9 | // import internal from 'rollup-plugin-internal'; 10 | 11 | import pkg from './package.json'; 12 | 13 | export default { 14 | input: '../example/src/lib/index.tsx', 15 | output: [{ 16 | file: pkg.main, 17 | format: 'cjs', 18 | exports: 'named', 19 | sourcemap: true 20 | }, 21 | { 22 | file: pkg.module, 23 | format: 'es', 24 | exports: 'named', 25 | sourcemap: true 26 | } 27 | ], 28 | plugins: [ 29 | external(), 30 | postcss({ 31 | modules: true 32 | }), 33 | url(), 34 | svgr(), 35 | resolve(), 36 | typescript({ 37 | rollupCommonJSResolveHack: true, 38 | clean: true 39 | }), 40 | commonjs() 41 | ] 42 | } -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDirs": [ 4 | "../example/src/lib" 5 | ], 6 | "outDir": "build", 7 | "module": "esnext", 8 | "target": "es5", 9 | "lib": [ 10 | "es6", 11 | "dom", 12 | "es2016", 13 | "es2017" 14 | ], 15 | "sourceMap": true, 16 | "allowJs": false, 17 | "jsx": "react", 18 | "declaration": true, 19 | "moduleResolution": "node", 20 | "forceConsistentCasingInFileNames": true, 21 | "noImplicitReturns": true, 22 | "noImplicitThis": true, 23 | "noImplicitAny": true, 24 | "strictNullChecks": true, 25 | "suppressImplicitAnyIndexErrors": true, 26 | "noUnusedLocals": false, 27 | "noUnusedParameters": false, 28 | "isolatedModules": false, 29 | "allowSyntheticDefaultImports": true, 30 | "downlevelIteration": true 31 | }, 32 | "include": [ 33 | "../example/src/lib" 34 | ], 35 | "exclude": [ 36 | "node_modules", 37 | "build", 38 | "dist", 39 | "example", 40 | "rollup.config.js" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "example", 5 | "lib" 6 | ], 7 | "version": "0.0.2-0" 8 | } 9 | --------------------------------------------------------------------------------