├── .gitignore ├── .markdownlint.json ├── LICENSE ├── README.md ├── index.html ├── lib └── main.ts ├── package.json ├── src ├── App.vue ├── VueScrollingTable.vue └── main.ts ├── tsconfig.json ├── vite.config.js └── vue-shim.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | package-lock.json 6 | debug.log 7 | .editorconfig 8 | *.tgz -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-hard-tabs": { 3 | "code_blocks": false 4 | }, 5 | "line-length": { 6 | "line_length": 100, 7 | "tables": false 8 | }, 9 | "no-bare-urls": false 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Richard Tallent 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 | # vue-scrolling-table 2 | 3 | > A Vue component to create tables with vertical and horizontal scrolling. Flexbox-based. 4 | 5 | ## Demo 6 | 7 | There is a live demo here: 8 | https://tallent.us/vue-scrolling-table 9 | 10 | The demo will allow you to play with various options. 11 | 12 | The repo for the demo application is here: 13 | https://github.com/richardtallent/vue-scrolling-table-sample 14 | 15 | ## Intro 16 | 17 | I recently needed a Vue component for a data grid in a desktop application I'm building. No need for 18 | responsiveness--in this case, I just need one huge table that scrolls vertically and horizontally, 19 | like a spreadsheet. I also needed the table to fit itself neatly into a flexbox layout, taking up 20 | any available space. 21 | 22 | "No problem, this is late 2017, we have modern browsers with CSS2 `sticky`!" Nope. Browser support 23 | for sticky is still buggy and incomplete, and I also needed the solution to work on IE11 to support 24 | outdated corporate environments. 25 | 26 | I found some very nice datagrid components, but none that suported everything on my wish list: 27 | 28 | - Flexbox sizing 29 | - Horizontal and vertical scrolling body 30 | - Flexibility to render my `` and `` cells however I want (which, for me, probably means 31 | lots of custom renderers, many of which will probably be Vue components of their own). 32 | - No built-in data model (I'd rather implement sorting, paging, etc. myself) 33 | - English documentation (Vue has global popularity, which is awesome, but occasionally that means 34 | some great components are out of reach.) 35 | 36 | So, I wrote this component. It is purposefully bare-bones, only drawing the table elements and 37 | synchronizing the horizontal scrolling of the header and body. It doesn't render any ``, ``, 38 | or `` elements itself. Instead, your parent component/application should render those using 39 | named slots. This gives you complete control over how those are handled, and allows this component 40 | to just focus on its sole task of making the table fit its parent and allowing the body to scroll. 41 | 42 | If you're curious, before creating the Vue component, the proof of concept was done on CodePen: 43 | https://codepen.io/richardtallent/pen/rpWBQK 44 | 45 | ## Example Usage 46 | 47 | In your `main.js`, if you want to globally register the component and its CSS: 48 | 49 | ```JavaScript 50 | import VueScrollingTable from "vue-scrolling-table" 51 | import "/node_modules/vue-scrolling-table/dist/style.css" 52 | //... 53 | createApp(App) 54 | .component(VueScrollingTable.name, VueScrollingTable) 55 | .mount("#app") 56 | ``` 57 | 58 | In your template: 59 | 60 | ```HTML 61 | 62 | 69 | 76 | 77 | ``` 78 | 79 | ## Properties 80 | 81 | ### deadAreaColor 82 | 83 | This is a **string** value. The default is `#CCC`. This is the color used for the "dead area" within 84 | any scrolling table that isn't used for the table contents. This dead area is possible because 85 | the table fits its parent container, but the rows or columns may not fill the entire space. This 86 | property accepts any legal CSS color expression (triplets, `rgb()`, etc.). 87 | 88 | ### includeFooter 89 | 90 | Boolean, defaults to `false`. Set this to `true` if you are providing content for a `tfoot` slot, 91 | otherwise the element will not be rendered. 92 | 93 | ### syncHeaderScroll 94 | 95 | Boolean, defaults to `true`. Set to `false` if you _do not_ want your header to scroll automatically 96 | when the user scrolls the body horizontally. 97 | 98 | ### syncFooterScroll 99 | 100 | Boolean, defaults to `true`. Set to `false` if you _do not_ want your footer to scroll automatically 101 | when the user scrolls the body horizontally. 102 | 103 | ### scrollHorizontal 104 | 105 | Boolean, defaults to `true`. Set to `false` if you _do not_ want the user to be able to scroll the 106 | body content horizontally (any overflow will be hidden). 107 | 108 | ### scrollVertical 109 | 110 | Boolean, defaults to `true`. Set to `false` if you _do not_ want the user to be able to scroll the 111 | body content vertically (any overflow will be hidden). 112 | 113 | ## Slots 114 | 115 | To render your actual rows and cells, you'll be using _named slots_. This gives you full control 116 | of how the table contents are rendered. 117 | 118 | ### thead 119 | 120 | Required. Use this slot to inject the `` element's contents. The component will freeze it at 121 | the top, and will synchronize its horizontal scrolling with `` scroll (there may be a short 122 | delay). 123 | 124 | ### tbody 125 | 126 | Required. Use this slot to inject the `` element's contents. The component will make it 127 | scrollable. 128 | 129 | ### tfoot 130 | 131 | Optional. Use this slot if you want to inject contents for a `` element. The component will 132 | freeze it at the bottom, below the scrolled ``. For now, this element is not scrolled 133 | automatically with the body. If you include this, you'll also need to set the `includeFooter` prop 134 | to `true` so the component knows to render the `` element. 135 | 136 | ## Events 137 | 138 | A `scroll` event is emitted by this component when the user scrolls the body. This event passes 139 | four arguments: the `` `scrollTop`, `scrollLeft`, `scrollHeight`, and `scrollWidth`. You 140 | can use this to, for example, show icons indicating that the user can scroll (useful when the 141 | browser doesn't display a scrollbar). Since this is fired based on the DOM `scroll` event, the 142 | same usual caveat applies: this is a high-frequency event, so try not to do anything complicated 143 | in response (if you need to do so, debounce the events and/or use `requestAnimationFrame`). 144 | 145 | A `header-dragover` event is emitted as the user drags a draggable element around over the `THEAD` 146 | element. This may be needed to, for example, implement resizable columns. The `preventDefault` 147 | call is made automatically by this component. 148 | 149 | A `header-dragenter` event is emitted when the user drags a draggable element into the `THEAD` 150 | element. This may be needed to, for example, implement resizable columns. 151 | 152 | A `header-drop` event is emitted when the user drops a draggable element on the `THEAD` element. 153 | This may be needed to, for example, implement resizable columns. 154 | 155 | ## Browser Compatibility 156 | 157 | This component is compatible with modern browsers. It may be compatible with older browsers, but 158 | I don't test on them. 159 | 160 | ## Slot Markup and Styling Requirements 161 | 162 | An important requirement of this component is that **all `` and `` cells** must have a 163 | **specific width** set for them, either via CSS classes or style attributes. Cells can't auto-size 164 | based on contents because that would leave the header and body cells with different widths. 165 | 166 | While it's theoretically possible to update the header column widths to match the body and vice 167 | versa, it's tricky, because unlike with scrolling, there are _many events_ that can result in a 168 | table cell resize (content change, CSS change, window resize, layout resize, etc.). Most 169 | implementations, including one I've done in the past, just end up polling on a timer and checking 170 | for columns to resize. 171 | 172 | You can implement this sort of column-width-tracking in your parent component if you want, but 173 | otherwise, you'll need to set the `width`, `min-width`, and `max-width` for all `` and `` 174 | cells to guarantee the width of all rows for a given column are the same. By default, they are 175 | all set to `10em`. While you can't use percentage units, depending on your layout, you can use 176 | `vw` units to achieve a similar scaled effect. 177 | 178 | ## Customizing the Style 179 | 180 | What little default styling is provided on the table is purposefully _very_ basic, and is not 181 | scoped, so it's easy to override in your calling application. Use `table.scrolling` as the base 182 | selector. 183 | 184 | ## How do I "freeze" a column? 185 | 186 | Here's some sample CSS for freezing the first column in a table. Unfortunately, it only works 187 | in Chrome and Safari as of December 2017: 188 | 189 | ```CSS 190 | table.scrolling td:first-child, 191 | table.scrolling th:first-child { 192 | position: -webkit-sticky; 193 | position: sticky; 194 | left: 0; 195 | } 196 | ``` 197 | 198 | Supporting this in every browser by simulating `sticky` is theoretically possible, but much 199 | more difficult than the scrolling implemented by this component due to differences in row 200 | height, etc. that would happen if the first column is removed from the normal flow to, say, 201 | use absolute positioning and update the scroll position with Javascript. 202 | 203 | ## Future plans 204 | 205 | I plan to actually use this on an upcoming project at work. It will be a good torture-test 206 | for the component. Some features I'm considering: 207 | 208 | - [x] Emitting events when the tbody is scrolled, so the caller can do other things. 209 | - [x] Optional footer scrolling. 210 | - [ ] Get rid of the need for the includeFooter prop. 211 | - [x] Option to disable/enable scrolling in either direction. 212 | - [ ] Avoid creating extra block on right of header if browser doesn't show scroll bars. 213 | - [x] Add TypeScript declarations (anyone know how to make Vite do this on build?) 214 | 215 | I'm open to other ideas, as long as they don't limit the flexibility of using slots for 216 | the header, body, and footer. But if someone wants to _build_ a data grid component that 217 | has this as a dependency, I'm all for it. 218 | 219 | ## Build Setup 220 | 221 | ```bash 222 | # install dependencies 223 | npm install 224 | 225 | # build for production with minification 226 | npm run build 227 | ``` 228 | 229 | ## Release History 230 | 231 | | Date | Version | Notes | 232 | | ---------- | ------- | ------------------------------------------------------------------------------------ | 233 | | 2017.12.24 | 0.1.0 | First published version | 234 | | 2017.12.24 | 0.1.1 | Patch based on sample app deveopment | 235 | | 2017.12.24 | 0.1.2 | Fix: old version went to npm | 236 | | 2017.12.25 | 0.2.0 | Added lots of options, updated README, fixed some display bugs when less data shown. | 237 | | 2018.08-06 | 0.2.1 | Added `header-dragenter`, `header-dragover`, and `header-drop` events. | 238 | | 2018.08-06 | 0.2.2 | $emit. _sigh_ | 239 | | 2020.02.02 | 1.0.0 | Upgraded to Vue 3, Vite, TypeScript. BREAKING, DO NOT UPGRADE FOR VUE 2.x. | 240 | | 2020.02.02 | 1.0.1 | Fix CSS export? | 241 | | 2020.02.05 | 1.0.3 | Gave up on TS. Fix CSS export? | 242 | | 2020.02.10 | 1.0.4 | TS Fixed. CSS injection is not automatic for Vite, documented this. | 243 | | 2020.02.10 | 2.0.0 | Simplify implementation (requires Vue 3.2). Border/BG styles easier to override. | 244 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vue-scrolling-table 8 | 13 | 14 | 15 | 16 | 17 | 31 | 32 | 61 |
62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /lib/main.ts: -------------------------------------------------------------------------------- 1 | import VueScrollingTable from "../src/VueScrollingTable.vue" 2 | 3 | export default VueScrollingTable 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-scrolling-table", 3 | "version": "2.0.0", 4 | "description": "A Vue 3 component to create tables with vertical and horizontal scrolling. Flexbox-based.", 5 | "author": "richardtallent ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/richardtallent/vue-scrolling-table" 10 | }, 11 | "homepage": "https://tallent.us/vue-scrolling-table/", 12 | "private": false, 13 | "main": "./dist/vue-scrolling-table.umd.js", 14 | "module": "./dist/vue-scrolling-table.es.js", 15 | "types": "./dist/vue-scrolling-table.d.ts", 16 | "style": "./dist/style.css", 17 | "files": [ 18 | "dist" 19 | ], 20 | "exports": { 21 | ".": { 22 | "import": "./dist/vue-scrolling-table.es.js", 23 | "require": "./dist/vue-scrolling-table.umd.js" 24 | } 25 | }, 26 | "scripts": { 27 | "dev": "vite", 28 | "devs": "vite --https", 29 | "build": "vite build" 30 | }, 31 | "dependencies": { 32 | "vue": "^3.2.31" 33 | }, 34 | "devDependencies": { 35 | "@typescript-eslint/parser": "^5.14.0", 36 | "@vitejs/plugin-vue": "^2.2.4", 37 | "@vue/compiler-sfc": "^3.2.31", 38 | "autoprefixer": "^10.4.2", 39 | "eslint": "^8.11.0", 40 | "eslint-config-tabsanity": "^2.0.0", 41 | "eslint-plugin-prettier": "^4.0.0", 42 | "eslint-plugin-vue": "^8.5.0", 43 | "postcss": "^8.4.8", 44 | "prettier": "^2.5.1", 45 | "rollup-plugin-typescript2": "^0.31.2", 46 | "stylelint": "^14.5.3", 47 | "stylelint-config-standard": "^25.0.0", 48 | "typescript": "^4.6.2", 49 | "vite": "^2.8.6" 50 | }, 51 | "eslintConfig": { 52 | "extends": [ 53 | "plugin:vue/vue3-essential", 54 | "eslint:recommended", 55 | "@vue/prettier", 56 | "tabsanity" 57 | ], 58 | "parserOptions": { 59 | "parser": "@typescript-eslint/parser" 60 | } 61 | }, 62 | "prettier": { 63 | "useTabs": true, 64 | "semi": false, 65 | "singleQuote": false, 66 | "bracketSpacing": true, 67 | "trailingComma": "es5", 68 | "printWidth": 180 69 | }, 70 | "postcss": { 71 | "plugins": { 72 | "autoprefixer": {} 73 | } 74 | }, 75 | "stylelint": { 76 | "extends": "stylelint-config-standard", 77 | "exclude": [ 78 | "dist" 79 | ], 80 | "rules": { 81 | "indentation": "tab", 82 | "declaration-block-trailing-semicolon": null, 83 | "no-descending-specificity": null 84 | } 85 | }, 86 | "browserslist": [ 87 | "> 1%", 88 | "last 2 versions", 89 | "not ie < 11", 90 | "maintained node versions" 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 116 | 1550 | 1592 | -------------------------------------------------------------------------------- /src/VueScrollingTable.vue: -------------------------------------------------------------------------------- 1 | 21 | 61 | 156 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue" 2 | import App from "./App.vue" 3 | createApp(App).mount("#app") 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": false, 7 | "lib": ["esnext", "dom"], 8 | "types": ["vite/client"], 9 | "importHelpers": true, 10 | "esModuleInterop": true, 11 | "declaration": true, 12 | "baseUrl": ".", 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true 15 | }, 16 | "include": ["src/**/*.vue", "src/**/*.ts", "vue-shim.d.ts"], 17 | "exclude": ["node_modules", "dist"] 18 | } 19 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import vue from "@vitejs/plugin-vue" 2 | import typescript from "rollup-plugin-typescript2" 3 | const path = require("path") 4 | 5 | module.exports = { 6 | // ESBuild doesn't emit typescript declarations, so use typescript2 instead 7 | plugins: [vue(), typescript()], 8 | esbuild: false, 9 | build: { 10 | sourcemap: false, 11 | lib: { 12 | entry: path.resolve(__dirname, "lib/main.ts"), 13 | name: "VueScrollingTable", 14 | manifest: true, 15 | }, 16 | rollupOptions: { 17 | // make sure to externalize deps that shouldn't be bundled into your library 18 | external: ["vue"], 19 | output: { 20 | // Provide global variables to use in the UMD build for externalized deps 21 | globals: { 22 | vue: "Vue", 23 | }, 24 | }, 25 | }, 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /vue-shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { ComponentOptions } from "vue" 3 | const component: ComponentOptions 4 | export default component 5 | } 6 | --------------------------------------------------------------------------------