├── .gitignore ├── src ├── main.svelte ├── checkbox-cell.svelte ├── select-cell.svelte ├── textbox-cell.svelte ├── edit-history.js └── index.svelte ├── TODO.md ├── rollup.config.js ├── LICENSE.txt ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | yarn.lock 4 | package-lock.json 5 | index.mjs 6 | index.js 7 | -------------------------------------------------------------------------------- /src/main.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - Add accessible ways of resizing and reordering columns using only a keyboard 4 | - Add mechanism for loading chunks of data asynchronously so the entire data set does not need to be loaded at once 5 | - Add ability to affix columns and rows so they remain visible when scrolling horizontally or vertically -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from "rollup-plugin-svelte"; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import pkg from "./package.json"; 4 | 5 | const name = pkg.name 6 | .replace(/^(@\S+\/)?(svelte-)?(\S+)/, "$3") 7 | .replace(/^\w/, m => m.toUpperCase()) 8 | .replace(/-\w/g, m => m[1].toUpperCase()); 9 | 10 | export default { 11 | input: "src/index.svelte", 12 | output: [ 13 | { file: pkg.module, format: "es" }, 14 | { file: pkg.main, format: "umd", name } 15 | ], 16 | plugins: [svelte(), resolve()] 17 | }; 18 | -------------------------------------------------------------------------------- /src/checkbox-cell.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 29 | 30 |
31 | 36 |
37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Brian Simon (https://github.com/bsssshhhhhhh) 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-data-grid", 3 | "description": "High-performance svelte data table component for displaying huge datasets", 4 | "version": "3.0.1", 5 | "author": { 6 | "name": "Brian Simon", 7 | "email": "bsssshhhhhhh@gmail.com", 8 | "url": "https://github.com/bsssshhhhhhh" 9 | }, 10 | "homepage": "https://github.com/bsssshhhhhhh/svelte-data-grid", 11 | "svelte": "src/main.svelte", 12 | "module": "index.mjs", 13 | "main": "index.js", 14 | "scripts": { 15 | "build": "rollup -c", 16 | "dev": "rollup -wc", 17 | "prepublishOnly": "npm run build" 18 | }, 19 | "devDependencies": { 20 | "@rollup/plugin-node-resolve": "^8.0.1", 21 | "rollup": "^2.17.1", 22 | "rollup-plugin-commonjs": "^10.1.0", 23 | "rollup-plugin-svelte": "^5.1.1", 24 | "svelte": "^3.23.2" 25 | }, 26 | "keywords": [ 27 | "svelte", 28 | "data-grid", 29 | "data-table", 30 | "table", 31 | "virtual-list" 32 | ], 33 | "repository": "bsssshhhhhhh/svelte-data-grid", 34 | "license": "MIT", 35 | "files": [ 36 | "src", 37 | "README.md", 38 | "LICENSE.txt" 39 | ], 40 | "dependencies": { 41 | "debounce": "^1.2.0", 42 | "deep-diff": "^1.0.2", 43 | "detect-browser": "^5.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/select-cell.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | 44 | 45 |
46 | {#if column.options instanceof Array} 47 | 56 | {/if} 57 |
58 | -------------------------------------------------------------------------------- /src/textbox-cell.svelte: -------------------------------------------------------------------------------- 1 | 68 | 69 | 90 | 91 |
92 | 98 |
99 | -------------------------------------------------------------------------------- /src/edit-history.js: -------------------------------------------------------------------------------- 1 | import DeepDiff from 'deep-diff'; 2 | const applyChange = DeepDiff.applyChange; 3 | const diff = DeepDiff.diff; 4 | /** 5 | * Edit history tracker for a javascript object using deep-diff to generate and apply patches 6 | */ 7 | export default class EditHistory { 8 | /** 9 | * Instantiates an instance of EditHistory 10 | * @param {Object} obj The object or array to track 11 | */ 12 | constructor(obj) { 13 | this.obj = JSON.parse(JSON.stringify(obj)); 14 | 15 | // initialize arrays for forwards and backwards patches 16 | this.forward = []; 17 | this.backward = []; 18 | } 19 | 20 | /** 21 | * Clears all forward and backward patches 22 | */ 23 | clear() { 24 | this.forward = []; 25 | this.backward = []; 26 | } 27 | 28 | /** 29 | * Records a change to an object 30 | * @param {Object} newObj The new object 31 | */ 32 | recordChange(newObj) { 33 | const patch = { 34 | redo: diff(this.obj, newObj), 35 | undo: diff(newObj, this.obj) 36 | }; 37 | 38 | if (!patch.redo || !patch.undo) { 39 | console.warn("Objects could not be diffed"); 40 | } else { 41 | this.obj = JSON.parse(JSON.stringify(newObj)); 42 | this.backward.push(patch); 43 | } 44 | } 45 | 46 | /** 47 | * Applies the most recent undo patch and returns the new object 48 | * @returns {Object} The tracked object 49 | */ 50 | undo() { 51 | if (this.backward.length === 0) { 52 | return null; 53 | } 54 | 55 | // grab the most recent backwards patch 56 | const patch = this.backward.pop(); 57 | 58 | // applyChange doesn't accept arrays, only its members 59 | patch.undo.forEach(x => applyChange(this.obj, x)); 60 | 61 | // put the patch into the forward queue 62 | this.forward.push(patch); 63 | 64 | return JSON.parse(JSON.stringify(this.obj)); 65 | } 66 | 67 | /** 68 | * Applies the most recent redo patch and returns the new object 69 | * @returns {Object} The tracked object 70 | */ 71 | redo() { 72 | if (this.forward.length === 0) { 73 | return null; 74 | } 75 | 76 | // grab the most recent forwards patch 77 | const patch = this.forward.pop(); 78 | 79 | // applyChange doesn't accept arrays, only its members 80 | patch.redo.forEach(x => applyChange(this.obj, x)); 81 | 82 | // put the patch into the backward queue 83 | this.backward.push(patch); 84 | 85 | return JSON.parse(JSON.stringify(this.obj)); 86 | } 87 | 88 | /** 89 | * Applies all the undo patches in the queue and returns the new object 90 | * @returns {Object} The tracked object 91 | */ 92 | undoAll() { 93 | while (this.backward.length > 0) { 94 | this.undo(); 95 | } 96 | 97 | return this.obj; 98 | } 99 | 100 | /** 101 | * Applies all the redo patches in the queue and returns the new object 102 | * @returns {Object} The tracked object 103 | */ 104 | redoAll() { 105 | while (this.forward.length > 0) { 106 | this.redo(); 107 | } 108 | 109 | return this.obj; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/svelte-data-grid.svg?style=flat-square)](https://npmjs.org/package/svelte-data-grid) 2 | # Svelte Data Grid 3 | 4 | ## [Demo](https://bsssshhhhhhh.github.io/svelte-data-grid-demo/) 5 | 6 | 7 | Svelte Data Grid is a svelte v3 component for displaying and editing any amount of data. 8 | 9 | ## Features: 10 | - Excellent scrolling performance 11 | - ARIA attributes set on elements 12 | - Lightweight even when displaying a huge dataset due to implementation of a "virtual list" mechanism 13 | - Column headers remain fixed at the top of the grid 14 | - Custom components can be specified to control how individual table cells or column headers are displayed 15 | - Columns can be resized and reordered 16 | 17 | ## Current Limitations: 18 | - Every row must have the same height and text cannot break onto the next line 19 | 20 | ## Usage: 21 | 22 | If using within Sapper: 23 | ``` 24 | npm install svelte-data-grid --save-dev 25 | ``` 26 | 27 | If using from inside a svelte component: 28 | ``` 29 | import DataGrid from "svelte-data-grid"; 30 | 31 | ``` 32 | 33 | If using from outside svelte: 34 | ``` 35 | import DataGrid from "svelte-data-grid"; 36 | const grid = new DataGrid({ 37 | target: document.querySelector('#my-grid-wrapper'), 38 | data: { 39 | rows: [ ... ], 40 | columns: [ ... ], 41 | allowResizeFromTableCells: true 42 | } 43 | }); 44 | 45 | grid.$on('columnOrderUpdated', () => { 46 | const { columns } = grid.get(); 47 | // save new column order 48 | }); 49 | ``` 50 | To learn more about using DataGrid outside of svelte, read [svelte's guide](https://svelte.dev/docs#Client-side_component_API) on how to interact with a svelte component. It is possible to integrate into any framework. 51 | 52 | DataGrid requires 2 properties to be passed in order to display data: `rows` and `columns`. 53 | 54 | `columns` is an array of objects containing at least 3 properties: `display`, `dataName`, and `width`. A svelte component can be specified in `headerComponent` and `cellComponent` if any custom cell behavior is required. 55 | 56 | ``` 57 | [ 58 | { 59 | display: 'Fruit Name', // What will be displayed as the column header 60 | dataName: 'fruitName', // The key of a row to get the column's data from 61 | width: 300, // Width, in pixels, of column 62 | disallowResize: true // Optional - disables resizing this column 63 | }, 64 | { 65 | display: 'Color', 66 | dataName: 'fruitColor', 67 | width: 600, 68 | myExtraData: 12345 69 | } 70 | ] 71 | ``` 72 | 73 | 74 | `rows` is an array of objects containing the data for each table row. 75 | 76 | ``` 77 | [ 78 | { 79 | fruitName: 'Apple', 80 | fruitColor: 'Red' 81 | }, 82 | { 83 | fruitName: 'Blueberry', 84 | fruitColor: 'Blue' 85 | }, 86 | { 87 | fruitName: 'Tomato', 88 | fruitColor: 'Red' 89 | } 90 | ] 91 | 92 | ``` 93 | 94 | ## Editing Data 95 | 96 | Version 2 added early support for editing data. Due to the lack of using a keyed each block to render the rows, maintaining focus on controls as the user scrolls is a tad wonky. This will be resolved in a future version. 97 | 98 | Import the components: 99 | ``` 100 | import TextboxCell from 'svelte-data-grid/src/textbox-cell.svelte'; 101 | import SelectCell from 'svelte-data-grid/src/select-cell.svelte'; 102 | import CheckboxCell from 'svelte-data-grid/src/checkbox-cell.svelte'; 103 | ``` 104 | 105 | ### Textbox Cell 106 | Textbox cell will debounce the user input, only recording changes after 400ms has elapsed since the user stops typing. 107 | ``` 108 | { 109 | display: 'Name', 110 | dataName: 'name', 111 | width: 250, 112 | cellComponent: TextboxCell 113 | } 114 | ``` 115 | 116 | ### Select Cell 117 | 118 | SelectCell requires that you provide an `options` array in your cell definition: 119 | ``` 120 | { 121 | display: 'Eye Color', 122 | dataName: 'eyeColor', 123 | width: 75, 124 | cellComponent: SelectCell, 125 | options: [ 126 | { 127 | display: 'Green', 128 | value: 'green' 129 | }, 130 | { 131 | display: 'Blue', 132 | value: 'blue' 133 | }, 134 | { 135 | display: 'Brown', 136 | value: 'brown' 137 | } 138 | ] 139 | } 140 | ``` 141 | 142 | ### Checkbox Cell 143 | CheckboxCell will set the checked state of the checkbox depending on the boolean value of the row's data. 144 | ``` 145 | { 146 | display: 'Active', 147 | dataName: 'isActive', 148 | width: 75, 149 | cellComponent: CheckboxCell 150 | } 151 | ``` 152 | 153 | 154 | ## Custom Cell Components 155 | 156 | Need to customize how your data is displayed or build more complex functionality into your grid? Specify `cellComponent` in your definition in the `columns` property. 157 | 158 | Components will be passed the following properties: 159 | - `rowNumber` - The index of the row within `rows` 160 | - `row` - The entire row object from `rows` 161 | - `column` - The entire column object from `columns` 162 | 163 | 164 | MyCustomCell.svelte 165 | ``` 166 | 174 | 175 |
176 | {row.data[column.dataName]} 177 |
178 | ``` 179 | 180 | Import the component 181 | ``` 182 | import MyCustomCell from './MyCustomCell.svelte'; 183 | ``` 184 | 185 | `columns` option: 186 | ``` 187 | [ 188 | { 189 | display: 'Fruit Color' 190 | dataName: 'fruitColor', 191 | width: 300, 192 | cellComponent: MyCustomCell 193 | } 194 | ] 195 | ``` 196 | 197 | ## Custom Header Components 198 | Header components can also be specified in `columns` entries as the `headerComponent` property. Header components are only passed `column`, the column object from `columns`. 199 | 200 | ## Options: 201 | 202 | Svelte Data Grid provides a few options for controlling the grid and its interactions: 203 | 204 | - `rowHeight` - The row height in pixels *(Default: 24)* 205 | - `allowResizeFromTableCells` - Allow user to click and drag the right border of a table cell to resize the column *(Default: false)* 206 | - `allowResizeFromTableHeaders` - Allow user to click and drag the right border of a column header to resize the column *(Default: true)* 207 | - `allowColumnReordering` - Allow user to drag a column header to move that column to a new position *(Default: true)* 208 | - `allowColumnAffix` - Allow user to drag the double line to affix columns to the left side of the grid. See section below for caveats *(Default: true if the browser is chrome, false otherwise)* 209 | - `__extraRows` - If it is desired that the virtual list include more DOM rows than are visible, the number of extra rows can be specified in `__extraRows` *(Default: 0)* 210 | - `__columnHeaderResizeCaptureWidth` The width of the element, in pixels, placed at the right border of a column that triggers that column's resize. *(Default: 20)* 211 | 212 | 213 | ## Events: 214 | - `columnOrderUpdated` - Fired when the user has dragged a column to a new position. The updated column order can be accessed from `component.get().columns` 215 | - `columnWidthUpdated` - Fired when a user has resized a column. The updated column width can be accessed from `event.width` and the column index can be accessed from `event.idx` 216 | 217 | ## Column Affixing 218 | 219 | This feature works well on Chrome because Chrome's scroll events are not fired asynchronously from the scroll action. Firefox, Edge, and IE all fire scroll events *after* the overflow container has scroll on screen. This causes a jittery effect that we cannot easily work around while providing a cross-browser solution. 220 | 221 | To fix the jitteriness on Firefox, a setting in about:config can be changed to turn off APZ. Set `layers.async-pan-zoom.enabled` to `false`. Obviously this is not a solution we can reasonably ask users to try, so I'm looking for other solutions. 222 | 223 | ## Bugs? Suggestions? 224 | Feedback is always appreciated. Feel free to open a GitHub issue if DataGrid doesn't work the way you expect or want it to. 225 | -------------------------------------------------------------------------------- /src/index.svelte: -------------------------------------------------------------------------------- 1 | 737 | 738 | 888 | 889 | 893 |
898 | {#if __resizing || __columnDragging || __affixingColumn} 899 |
902 | {/if} 903 | {#if __affixingRow} 904 |
905 | {/if} 906 | 907 |
908 | 911 |
915 | {#each columns as column, i (i)} 916 |
onColumnDragStart(event, i)} 919 | style="z-index: {getCellZIndex(__affixedColumnIndices, i)}; left: {getCellLeft( 920 | { i, columnWidths, __affixedColumnIndices, __scrollLeft } 921 | )}px; width: {columnWidths[i]}px; height: {rowHeight}px; line-height: {rowHeight}px;" 922 | title={column.display || ''} 923 | use:dragCopy={allowColumnReordering} 924 | role="columnheader"> 925 | {#if column.headerComponent} 926 | 927 | {:else} 928 |
{column.display || ''}
929 | {/if} 930 |
931 | {#if allowResizeFromTableHeaders && !column.disallowResize} 932 |
onColumnResizeStart(event, i)} /> 942 | {/if} 943 | {/each} 944 |
945 |
946 | 947 |
954 | 955 | {#if allowColumnAffix} 956 |
960 | {/if} 961 |
965 | 966 | 967 |
970 | {#if allowResizeFromTableCells} 971 | {#each columns as column, i} 972 | {#if !column.disallowResize} 973 |
onColumnResizeStart(event, i)} /> 982 | {/if} 983 | {/each} 984 | {/if} 985 |
986 | 987 | 988 | 989 | {#each visibleRows as row, i} 990 |
996 | {#each columns as column, j} 997 |
1003 | {#if column.cellComponent} 1004 | 1010 | {:else} 1011 |
{row.data[column.dataName] || ''}
1012 | {/if} 1013 |
1014 | {/each} 1015 |
1016 | {/each} 1017 |
1018 |
1019 | --------------------------------------------------------------------------------