├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── docs.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── docs ├── demos │ ├── 1-simple │ │ └── index.html │ ├── 10-filters │ │ └── index.html │ ├── 11-export │ │ └── index.html │ ├── 12-updating │ │ └── index.html │ ├── 13-hide-column │ │ └── index.html │ ├── 14-row-navigation │ │ └── index.html │ ├── 15-editing │ │ └── index.html │ ├── 16-scroll-y │ │ └── index.html │ ├── 17-checkbox-column │ │ ├── datatable.json │ │ └── index.html │ ├── 18-fetch-api │ │ ├── data.json │ │ └── index.html │ ├── 19-bootstrap-table │ │ └── index.html │ ├── 2-dynamic-import │ │ └── index.html │ ├── 20-column-filters │ │ └── index.html │ ├── 21-svg │ │ └── index.html │ ├── 22-and-search │ │ └── index.html │ ├── 23-column-filter-button │ │ └── index.html │ ├── 24-footer │ │ └── index.html │ ├── 25-cell-attributes │ │ ├── data.json │ │ └── index.html │ ├── 26-numeric-sort │ │ └── index.html │ ├── 27-load-json │ │ ├── additional_data.json │ │ ├── index.html │ │ └── initial_data.json │ ├── 28-sort-order-attribute │ │ └── index.html │ ├── 29-responsive-layout │ │ └── index.html │ ├── 3-cdn │ │ └── index.html │ ├── 30-custom-search-method │ │ ├── index.html │ │ ├── script.js │ │ └── style.css │ ├── 4-render-column-cell │ │ └── index.html │ ├── 5-column-manipulation │ │ ├── datatable.column.json │ │ ├── datatable.json │ │ └── index.html │ ├── 6-datetime-sorting │ │ └── index.html │ ├── 7-init-destroy-import-export │ │ └── index.html │ ├── 8-add-1000-rows │ │ └── index.html │ ├── 9-add-1000-rows-without-conversion │ │ └── index.html │ ├── demo.css │ └── index.html ├── documentation │ ├── API.md │ ├── Adding-a-column-from-a-remote-source.md │ ├── Dynamically-adding-data.md │ ├── Events.md │ ├── Getting-Started.md │ ├── Options.md │ ├── Upgrading.md │ ├── caption.md │ ├── classes.md │ ├── columns-API.md │ ├── columns.md │ ├── data.md │ ├── datetime.md │ ├── destroy().md │ ├── destroyable.md │ ├── diffDomOptions.md │ ├── ellipsisText.md │ ├── firstLast.md │ ├── firstText.md │ ├── fixedColumns.md │ ├── fixedHeight.md │ ├── footer.md │ ├── header.md │ ├── hiddenHeader.md │ ├── index.md │ ├── init().md │ ├── insert().md │ ├── labels.md │ ├── lastText.md │ ├── multiSearch().md │ ├── nextPrev.md │ ├── nextText.md │ ├── on().md │ ├── page().md │ ├── pagerDelta.md │ ├── pagerRender.md │ ├── paging.md │ ├── perPage.md │ ├── perPageSelect.md │ ├── prevText.md │ ├── print().md │ ├── refresh().md │ ├── rowNavigation.md │ ├── rowRender.md │ ├── rowSelectionKeys.md │ ├── rows-API.md │ ├── scrollY.md │ ├── search().md │ ├── searchMethod.md │ ├── searchQuerySeparator.md │ ├── searchable.md │ ├── setMessage().md │ ├── sortable.md │ ├── tabIndex.md │ ├── tableRender.md │ ├── template.md │ ├── truncatePager.md │ └── update().md ├── favicon.ico ├── favicon.png ├── favicon.svg └── index.html ├── package.json ├── rollup.config.mjs ├── rollup.demos.config.mjs ├── src ├── column_filter │ ├── column_filter.ts │ ├── config.ts │ ├── index.ts │ └── types.ts ├── column_settings.ts ├── columns.ts ├── config.ts ├── convert │ ├── csv.ts │ ├── index.ts │ └── json.ts ├── css │ ├── column_filter.css │ ├── editing.css │ └── style.css ├── datatable.ts ├── date.ts ├── editing │ ├── config.ts │ ├── editor.ts │ ├── index.ts │ └── types.ts ├── export │ ├── csv.ts │ ├── index.ts │ ├── json.ts │ ├── sql.ts │ └── txt.ts ├── helpers.ts ├── index.ts ├── read_data.ts ├── rows.ts ├── templates.ts ├── types.ts ├── virtual_dom.ts └── virtual_pager_dom.ts ├── test ├── cases │ ├── cell-attributes-dom.html │ ├── cell-attributes-js.html │ ├── empty-table-with-footer.html │ └── multiple-classes.html ├── server.mjs └── test.mjs └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | docs/demos/dist/ 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | docs/demos/dist/* linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] I have looked through the [documentation](https://fiduswriter.github.io/simple-datatables/documentation/) to try to see if this behavior is documented. 11 | - [ ] I have looked at the [demos](https://fiduswriter.github.io/simple-datatables/demos/) to see if one of them handles this, but none of them did. 12 | - [ ] I have created a [jsfiddle](https://jsfiddle.net/) here: [INSERT LINK] that demonstrates the issue. 13 | 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To Reproduce** 19 | 20 | Steps to reproduce the behavior: 21 | 1. Go to '...' 22 | 2. Click on '....' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | 26 | **Expected behavior** 27 | A clear and concise description of what you expected to happen. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | - [ ] I have looked through the [documentation](https://fiduswriter.github.io/simple-datatables/documentation/) to try to see if this feature already exists. 10 | - [ ] I have looked at the [demos](https://fiduswriter.github.io/simple-datatables/demos/) to see if one of them does what I want, but none of them did. 11 | 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Update docs 2 | on: 3 | push: 4 | branches: ["main"] 5 | workflow_dispatch: 6 | 7 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 14 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: false 18 | 19 | jobs: 20 | # Build job 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | - name: Setup Pages 27 | uses: actions/configure-pages@v5 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 20 32 | - name: Build JS bundle 33 | run: npm install 34 | - name: Build site 35 | uses: actions/jekyll-build-pages@v1 36 | with: 37 | source: ./docs 38 | destination: ./_site 39 | - name: Upload artifact 40 | uses: actions/upload-pages-artifact@v3 41 | 42 | # Deployment job 43 | deploy: 44 | environment: 45 | name: github-pages 46 | url: ${{ steps.deployment.outputs.page_url }} 47 | runs-on: ubuntu-latest 48 | needs: build 49 | steps: 50 | - name: Deploy to GitHub Pages 51 | id: deployment 52 | uses: actions/deploy-pages@v4 53 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Check lints 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | - name: Setup Node.js 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: 20 15 | - name: Install dependencies 16 | run: npm install 17 | - name: Lints 18 | run: npm run lint 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Check tests 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | - name: Setup Chrome 12 | run: sudo apt update && sudo apt install -y google-chrome-stable 13 | - run: /usr/bin/google-chrome-stable --version 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | - name: Install dependencies 19 | run: npm install 20 | - uses: jamesmortensen/chromedriver-matcher-action@v1 21 | - name: Tests 22 | run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /docs/demos/dist/ 3 | node_modules/ 4 | tests/qunit.css 5 | package-lock.json 6 | pnpm-lock.yaml 7 | yarn.lock 8 | .DS_Store 9 | .idea/ 10 | .python-version 11 | .direnv 12 | .envrc 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | detect_chromedriver_version=true 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-datatables 2 | 3 | A lightweight, extendable, JavaScript HTML table library written in TypeScript and transpilled to Vanilla JavaScript. Similar to jQuery DataTables **for use in modern browsers**, but without the jQuery dependency. 4 | 5 | ## Examples / Demos 6 | 7 | See the demos [here](https://fiduswriter.github.io/simple-datatables/demos/) and the documentation [here](https://fiduswriter.github.io/simple-datatables/documentation/). 8 | 9 | ### Upgrading 10 | 11 | For upgrading from one major version to another, check the upgrade guide: 12 | https://fiduswriter.github.io/simple-datatables/documentation/Upgrading 13 | 14 | **Note**: The upgrade from version 5 version 6 is the most complicated upgrade so far. Please read through the instructions before filing complaints. If you run simple-datatables from a CDN, make sure that you have fixed it to a specific major or minor version so that you do not accidentally upload to a new version that requires you to do lots of manual adjustments. 15 | 16 | 17 | # CDN 18 | 19 | To use the CDN version of simple-datatables use either [https://cdn.jsdelivr.net/npm/simple-datatables@latest](https://cdn.jsdelivr.net/npm/simple-datatables@latest) or [https://unpkg.com/simple-datatables](https://unpkg.com/simple-datatables). You also need to add the CSS styling, so the elements you'll add to html head element can for example be these: 20 | 21 | **Note:** For production websites, specify a specific major version. For example [https://cdn.jsdelivr.net/npm/simple-datatables@6](https://cdn.jsdelivr.net/npm/simple-datatables@6) for the latest version in the 6.x.x series or [https://cdn.jsdelivr.net/npm/simple-datatables@6.0](https://cdn.jsdelivr.net/npm/simple-datatables@6.0) for the latest version in the 6.0.x series. 22 | 23 | ```html 24 | 25 | 26 | ``` 27 | ### License 28 | 29 | LGPL 30 | 31 | ### Features 32 | 33 | * Sortable/filterable columns 34 | * Pagination 35 | * Searchable 36 | * Customisable layout 37 | * Customisable labels 38 | * Customise column rendering 39 | * Export to common formats like `csv`, `txt`, `json`, and `sql` 40 | * Import `csv` and `json` data 41 | * Control column visibility 42 | * Reorder or swap columns 43 | * dayjs integration for sorting columns with datetime strings 44 | * Using [diffDOM](https://github.com/fiduswriter/diffDOM) for faster DOM updates. 45 | 46 | 47 | [simple-datatables Documentation](https://fiduswriter.github.io/simple-datatables/documentation) 48 | 49 | 50 | ### History 51 | 52 | This project started as a fork of [Vanilla-DataTables](https://github.com/Mobius1/Vanilla-DataTables), but it has since been converted to TypeScript. 53 | 54 | If you want a version that works in very old browsers (IE, etc.), then head over to https://github.com/fiduswriter/simple-datatables-classic . 55 | 56 | 57 | --- 58 | 59 | 60 | ### Install 61 | 62 | ## npm 63 | ``` 64 | npm install simple-datatables --save 65 | ``` 66 | ## Yarn 67 | ``` 68 | yarn add simple-datatables 69 | ``` 70 | 71 | --- 72 | 73 | ### Quick Start 74 | 75 | Then just initialise the plugin by import DataTable and either passing a reference to the table or a CSS3 selector string as the first parameter: 76 | 77 | ```javascript 78 | import {DataTable} from "simple-datatables" 79 | 80 | const myTable = document.querySelector("#myTable"); 81 | const dataTable = new DataTable(myTable); 82 | 83 | // or 84 | 85 | const dataTable = new DataTable("#myTable"); 86 | 87 | ``` 88 | 89 | You can also pass the options object as the second parameter: 90 | 91 | ```javascript 92 | import {DataTable} from "simple-datatables" 93 | 94 | const dataTable = new DataTable("#myTable", { 95 | searchable: false, 96 | fixedHeight: true, 97 | ... 98 | }) 99 | ``` 100 | 101 | If using the CDN: 102 | 103 | ```javascript 104 | const dataTable = new simpleDatatables.DataTable("#myTable", { 105 | searchable: false, 106 | fixedHeight: true, 107 | ... 108 | }) 109 | ``` 110 | 111 | --- 112 | 113 | ## How to contribute? 114 | 115 | 1. Fork the repository 116 | 2. Create a sub-branch 117 | 3. Clone the sub-branch to your local system 118 | 4. Install [NodeJS](https://nodejs.org/en) 119 | 5. Open the project in a code editor (for example [Visual Studio Code](https://code.visualstudio.com/) or [Pulsar Edit](https://pulsar-edit.dev/)) 120 | 6. Open the Terminal 121 | 7. Run `npm install` in the Terminal 122 | 8. Start making changes and contributing to the project 🙂 123 | 9. You can run `npm run test_server` to test your code. This runs on port 3000 (http://localhost:3000/) 124 | 10. You can also run `npm run build` in the Terminal to build the final files 125 | 11. Once finished, then commit/push your code and create a Pull Request on GitHub for the changes 126 | -------------------------------------------------------------------------------- /docs/demos/10-filters/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Filters - simple-datatables 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

16 | simple-datatables 17 |

18 | Documentation 19 | Demos 20 |
21 | 22 |

Filters

23 | 24 | 25 | 26 | 27 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/demos/12-updating/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Updating - simple-datatables 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

16 | simple-datatables 17 |

18 | Documentation 19 | Demos 20 |
21 | 22 |

Updating

23 |

24 | Replace first match in a specified column. 25 |

26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 | 37 | 38 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /docs/demos/17-checkbox-column/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Checkbox column - simple-datatables 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 |
20 |

21 | simple-datatables 22 |

23 | Documentation 24 | Demos 25 |
26 | 27 |

Checkbox column

28 | 29 |
30 | 31 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /docs/demos/18-fetch-api/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": 1, 4 | "Drink": "latte", 5 | "Caffeinated": false, 6 | "Price": 4, 7 | "Profit": 0 8 | }, 9 | { 10 | "ID": 2, 11 | "Drink": "herbal tea", 12 | "Caffeinated": false, 13 | "Price": 3, 14 | "Profit": 0.56 15 | }, 16 | { 17 | "ID": 3, 18 | "Drink": "green tea", 19 | "Caffeinated": true, 20 | "Price": 3, 21 | "Profit": 1.72 22 | }, 23 | { 24 | "ID": 4, 25 | "Drink": "latte", 26 | "Caffeinated": true, 27 | "Price": 3, 28 | "Profit": -1.21 29 | }, 30 | { 31 | "ID": 5, 32 | "Drink": "green tea", 33 | "Caffeinated": false, 34 | "Price": 3, 35 | "Profit": 0 36 | }, 37 | { 38 | "ID": 6, 39 | "Drink": "green tea", 40 | "Caffeinated": false, 41 | "Price": 3, 42 | "Profit": 0 43 | }, 44 | { 45 | "ID": 7, 46 | "Drink": "green tea", 47 | "Caffeinated": true, 48 | "Price": 3, 49 | "Profit": 1.72 50 | }, 51 | { 52 | "ID": 8, 53 | "Drink": "latte", 54 | "Caffeinated": true, 55 | "Price": 3, 56 | "Profit": 1.72 57 | }, 58 | { 59 | "ID": 9, 60 | "Drink": "green tea", 61 | "Caffeinated": true, 62 | "Price": 3, 63 | "Profit": -1.21 64 | }, 65 | { 66 | "ID": 10, 67 | "Drink": "green tea", 68 | "Caffeinated": false, 69 | "Price": 3, 70 | "Profit": 0 71 | }, 72 | { 73 | "ID": 11, 74 | "Drink": "green tea", 75 | "Caffeinated": true, 76 | "Price": 3, 77 | "Profit": 1.72 78 | }, 79 | { 80 | "ID": 12, 81 | "Drink": "green tea", 82 | "Caffeinated": true, 83 | "Price": 3, 84 | "Profit": 1.72 85 | }, 86 | { 87 | "ID": 13, 88 | "Drink": "latte", 89 | "Caffeinated": false, 90 | "Price": 3, 91 | "Profit": 0 92 | }, 93 | { 94 | "ID": 14, 95 | "Drink": "latte", 96 | "Caffeinated": true, 97 | "Price": 3, 98 | "Profit": 1.72 99 | }, 100 | { 101 | "ID": 15, 102 | "Drink": "green tea", 103 | "Caffeinated": false, 104 | "Price": 3, 105 | "Profit": 0 106 | }, 107 | { 108 | "ID": 16, 109 | "Drink": "green tea", 110 | "Caffeinated": true, 111 | "Price": 3, 112 | "Profit": 1.72 113 | }, 114 | { 115 | "ID": 17, 116 | "Drink": "latte", 117 | "Caffeinated": false, 118 | "Price": 3, 119 | "Profit": 0 120 | }, 121 | { 122 | "ID": 18, 123 | "Drink": "latte", 124 | "Caffeinated": true, 125 | "Price": 3, 126 | "Profit": -1.21 127 | }, 128 | { 129 | "ID": 19, 130 | "Drink": "latte", 131 | "Caffeinated": true, 132 | "Price": 3, 133 | "Profit": 1.72 134 | }, 135 | { 136 | "ID": 20, 137 | "Drink": "latte", 138 | "Caffeinated": false, 139 | "Price": 3, 140 | "Profit": 0 141 | }, 142 | { 143 | "ID": 21, 144 | "Drink": "latte", 145 | "Caffeinated": false, 146 | "Price": 3, 147 | "Profit": 0 148 | }, 149 | { 150 | "ID": 22, 151 | "Drink": "latte", 152 | "Caffeinated": true, 153 | "Price": 3, 154 | "Profit": 1.72 155 | }, 156 | { 157 | "ID": 23, 158 | "Drink": "green tea", 159 | "Caffeinated": true, 160 | "Price": 3, 161 | "Profit": -1.21 162 | }, 163 | { 164 | "ID": 24, 165 | "Drink": "green tea", 166 | "Caffeinated": true, 167 | "Price": 3, 168 | "Profit": -1.21 169 | }, 170 | { 171 | "ID": 25, 172 | "Drink": "green tea", 173 | "Caffeinated": true, 174 | "Price": 3, 175 | "Profit": -1.21 176 | } 177 | ] -------------------------------------------------------------------------------- /docs/demos/18-fetch-api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fetch API - simple-datatables 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

16 | simple-datatables 17 |

18 | Documentation 19 | Demos 20 |
21 | 22 |

Fetch API

23 | 24 |
25 | 26 | 27 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/demos/24-footer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Custom footer - simple-datatables 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

16 | simple-datatables 17 |

18 | Documentation 19 | Demos 20 |
21 | 22 |

Custom footer

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |
AthleteNationGoldSilverBronze
Michael PhelpsUnited States2332
Larisa LatyninaSoviet Union954
Paavo NurmiFinland930
Mark SpitzUnited States911
Carl LewisUnited States910
Marit BjørgenNorway843
Ole Einar BjørndalenNorway841
Bjørn DæhlieNorway840
Birgit FischerGermany840
Sawao KatoJapan831
This is a table footer.
Total993212
This is a table caption.
121 | 122 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /docs/demos/26-numeric-sort/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Numeric sort - simple-datatables 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

16 | simple-datatables 17 |

18 | Documentation 19 | Demos 20 |
21 | 22 |

Numeric sort

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
Number#
Minus one point five-1.5
Minus one-1
Minus zero point eight-0.8
Minus zero point five-0.5
Minus zero point two-0.2
Zero0
Zero point five0.5
Zero point eight0.8
One1
One poiny five1.5
Two2
Three3
Four4
Five5
Six6
Seven7
Eight8
Nine9
Ten10
Eleven11
113 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /docs/demos/27-load-json/additional_data.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name": "Lareina Jones", 3 | "extension": "8642", 4 | "city": "East Linton", 5 | "start_date": "2009/07/08" 6 | }, 7 | { 8 | "name": "Joshua Weiss", 9 | "extension": "2289", 10 | "city": "Saint-Léonard", 11 | "start_date": "2010/15/01" 12 | }, 13 | { 14 | "name": "Kiona Lowery", 15 | "extension": "5952", 16 | "city": "Inuvik", 17 | "start_date": "2002/17/12" 18 | }, 19 | { 20 | "name": "Nina Rush", 21 | "extension": "7567", 22 | "city": "Bo‘lhe", 23 | "start_date": "2008/27/01" 24 | }, 25 | { 26 | "name": "Palmer Parker", 27 | "extension": "2000", 28 | "city": "Stade", 29 | "start_date": "2012/24/07" 30 | }, 31 | { 32 | "name": "Vielka Olsen", 33 | "extension": "3745", 34 | "city": "Vrasene", 35 | "start_date": "2016/08/01" 36 | }, 37 | { 38 | "name": "Meghan Cunningham", 39 | "extension": "8604", 40 | "city": "Söke", 41 | "start_date": "2007/16/02" 42 | }, 43 | { 44 | "name": "Iola Shaw", 45 | "extension": "6447", 46 | "city": "Albany", 47 | "start_date": "2014/05/03" 48 | }, 49 | { 50 | "name": "Imelda Cole", 51 | "extension": "4564", 52 | "city": "Haasdonk", 53 | "start_date": "2007/16/11" 54 | }, 55 | { 56 | "name": "Jerry Beach", 57 | "extension": "6801", 58 | "city": "Gattatico", 59 | "start_date": "1999/07/07" 60 | }, 61 | { 62 | "name": "Garrett Rocha", 63 | "extension": "3938", 64 | "city": "Gavorrano", 65 | "start_date": "2000/06/08" 66 | }, 67 | { 68 | "name": "Derek Kerr", 69 | "extension": "1724", 70 | "city": "Gualdo Cattaneo", 71 | "start_date": "2014/21/01" 72 | }, 73 | { 74 | "name": "Shad Hudson", 75 | "extension": "5944", 76 | "city": "Salamanca", 77 | "start_date": "2014/10/12" 78 | }, 79 | { 80 | "name": "Daryl Ayers", 81 | "extension": "8276", 82 | "city": "Barchi", 83 | "start_date": "2012/12/11" 84 | }, 85 | { 86 | "name": "Caleb Livingston", 87 | "extension": "3094", 88 | "city": "Fatehpur", 89 | "start_date": "2014/13/02" 90 | }, 91 | { 92 | "name": "Sydney Meyer", 93 | "extension": "4576", 94 | "city": "Neubrandenburg", 95 | "start_date": "2015/06/02" 96 | }, 97 | { 98 | "name": "Lani Lawrence", 99 | "extension": "8501", 100 | "city": "Turnhout", 101 | "start_date": "2008/07/05" 102 | }, 103 | { 104 | "name": "Allegra Shepherd", 105 | "extension": "2576", 106 | "city": "Meeuwen-Gruitrode", 107 | "start_date": "2004/19/04" 108 | }, 109 | { 110 | "name": "Fallon Reyes", 111 | "extension": "3178", 112 | "city": "Monceau-sur-Sambre", 113 | "start_date": "2005/15/02" 114 | }, 115 | { 116 | "name": "Karen Whitley", 117 | "extension": "4357", 118 | "city": "Sluis", 119 | "start_date": "2003/02/05" 120 | }, 121 | { 122 | "name": "Stewart Stephenson", 123 | "extension": "5350", 124 | "city": "Villa Faraldi", 125 | "start_date": "2003/05/07" 126 | }, 127 | { 128 | "name": "Ursula Reynolds", 129 | "extension": "7544", 130 | "city": "Southampton", 131 | "start_date": "1999/16/12" 132 | }, 133 | { 134 | "name": "Adrienne Winters", 135 | "extension": "4425", 136 | "city": "Laguna Blanca", 137 | "start_date": "2014/15/09" 138 | }, 139 | { 140 | "name": "Francesca Brock", 141 | "extension": "1337", 142 | "city": "Oban", 143 | "start_date": "2000/12/06" 144 | }, 145 | { 146 | "name": "Ursa Davenport", 147 | "extension": "7629", 148 | "city": "New Plymouth", 149 | "start_date": "2013/27/06" 150 | }, 151 | { 152 | "name": "Mark Brock", 153 | "extension": "3310", 154 | "city": "Veenendaal", 155 | "start_date": "2006/08/09" 156 | }, 157 | { 158 | "name": "Dale Rush", 159 | "extension": "5050", 160 | "city": "Chicoutimi", 161 | "start_date": "2000/27/03" 162 | }, 163 | { 164 | "name": "Shellie Murphy", 165 | "extension": "3845", 166 | "city": "Marlborough", 167 | "start_date": "2013/13/11" 168 | }, 169 | { 170 | "name": "Porter Nicholson", 171 | "extension": "4539", 172 | "city": "Bismil", 173 | "start_date": "2012/22/10" 174 | }, 175 | { 176 | "name": "Oliver Huber", 177 | "extension": "1265", 178 | "city": "Hannche", 179 | "start_date": "2002/11/01" 180 | }, 181 | { 182 | "name": "Calista Maynard", 183 | "extension": "3315", 184 | "city": "Pozzuolo del Friuli", 185 | "start_date": "2006/23/03" 186 | }, 187 | { 188 | "name": "Lois Vargas", 189 | "extension": "6825", 190 | "city": "Cumberland", 191 | "start_date": "1999/25/04" 192 | }, 193 | { 194 | "name": "Hermione Dickson", 195 | "extension": "2785", 196 | "city": "Woodstock", 197 | "start_date": "2001/22/03" 198 | }, 199 | { 200 | "name": "Dalton Jennings", 201 | "extension": "5416", 202 | "city": "Dudzele", 203 | "start_date": "2015/09/02" 204 | }, 205 | { 206 | "name": "Cathleen Kramer", 207 | "extension": "3380", 208 | "city": "Crowsnest Pass", 209 | "start_date": "2012/27/07" 210 | }, 211 | { 212 | "name": "Zachery Morgan", 213 | "extension": "6730", 214 | "city": "Collines-de-l'Outaouais", 215 | "start_date": "2006/04/09" 216 | }, 217 | { 218 | "name": "Yoko Freeman", 219 | "extension": "4077", 220 | "city": "Lidköping", 221 | "start_date": "2002/27/12" 222 | }, 223 | { 224 | "name": "Chaim Waller", 225 | "extension": "4240", 226 | "city": "North Shore", 227 | "start_date": "2010/25/07" 228 | }, 229 | { 230 | "name": "Berk Johnston", 231 | "extension": "4532", 232 | "city": "Vergnies", 233 | "start_date": "2016/23/02" 234 | }, 235 | { 236 | "name": "Tad Munoz", 237 | "extension": "2902", 238 | "city": "Saint-Nazaire", 239 | "start_date": "2010/09/05" 240 | }, 241 | { 242 | "name": "Vivien Dominguez", 243 | "extension": "5653", 244 | "city": "Bargagli", 245 | "start_date": "2001/09/01" 246 | }, 247 | { 248 | "name": "Carissa Lara", 249 | "extension": "3241", 250 | "city": "Sherborne", 251 | "start_date": "2015/07/12" 252 | }, 253 | { 254 | "name": "Hammett Gordon", 255 | "extension": "8101", 256 | "city": "Wah", 257 | "start_date": "1998/06/09" 258 | }, 259 | { 260 | "name": "Walker Nixon", 261 | "extension": "6901", 262 | "city": "Metz", 263 | "start_date": "2011/12/11" 264 | }, 265 | { 266 | "name": "Nathan Espinoza", 267 | "extension": "5956", 268 | "city": "Strathcona County", 269 | "start_date": "2002/25/01" 270 | }, 271 | { 272 | "name": "Kelly Cameron", 273 | "extension": "4836", 274 | "city": "Fontaine-Valmont", 275 | "start_date": "1999/02/07" 276 | }, 277 | { 278 | "name": "Kyra Moses", 279 | "extension": "3796", 280 | "city": "Quenast", 281 | "start_date": "1998/07/07" 282 | }, 283 | { 284 | "name": "Grace Bishop", 285 | "extension": "8340", 286 | "city": "Rodez", 287 | "start_date": "2012/02/10" 288 | }, 289 | { 290 | "name": "Haviva Hernandez", 291 | "extension": "8136", 292 | "city": "Suwałki", 293 | "start_date": "2000/30/01" 294 | }, 295 | { 296 | "name": "Alisa Horn", 297 | "extension": "9853", 298 | "city": "Ucluelet", 299 | "start_date": "2007/01/11" 300 | }, 301 | { 302 | "name": "Zelenia Roman", 303 | "extension": "7516", 304 | "city": "Redwater", 305 | "start_date": "2012/03/03" 306 | }] -------------------------------------------------------------------------------- /docs/demos/27-load-json/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Load JSON - simple-datatables 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

16 | simple-datatables 17 |

18 | Documentation 19 | Demos 20 |
21 | 22 |

Load JSON

23 | 24 |
25 | 26 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/demos/27-load-json/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Unity Pugh", 4 | "extension": "9958", 5 | "city": "Curicó", 6 | "start_date": "2005/02/11" 7 | }, 8 | { 9 | "name": "Theodore Duran", 10 | "extension": "8971", 11 | "city": "Dhanbad", 12 | "start_date": "1999/04/07" 13 | }, 14 | { 15 | "name": "Kylie Bishop", 16 | "extension": "3147", 17 | "city": "Norman", 18 | "start_date": "2005/09/08" 19 | }, 20 | { 21 | "name": "Willow Gilliam", 22 | "extension": "3497", 23 | "city": "Amqui", 24 | "start_date": "2009/29/11" 25 | }, 26 | { 27 | "name": "Blossom Dickerson", 28 | "extension": "5018", 29 | "city": "Kempten", 30 | "start_date": "2006/11/09" 31 | }, 32 | { 33 | "name": "Elliott Snyder", 34 | "extension": "3925", 35 | "city": "Enines", 36 | "start_date": "2006/03/08" 37 | }, 38 | { 39 | "name": "Castor Pugh", 40 | "extension": "9488", 41 | "city": "Neath", 42 | "start_date": "2014/23/12" 43 | }, 44 | { 45 | "name": "Pearl Carlson", 46 | "extension": "6231", 47 | "city": "Cobourg", 48 | "start_date": "2014/31/08" 49 | }, 50 | { 51 | "name": "Deirdre Bridges", 52 | "extension": "1579", 53 | "city": "Eberswalde-Finow", 54 | "start_date": "2014/26/08" 55 | }, 56 | { 57 | "name": "Daniel Baldwin", 58 | "extension": "6095", 59 | "city": "Moircy", 60 | "start_date": "2000/11/01" 61 | }, 62 | { 63 | "name": "Phelan Kane", 64 | "extension": "9519", 65 | "city": "Germersheim", 66 | "start_date": "1999/16/04" 67 | }, 68 | { 69 | "name": "Quentin Salas", 70 | "extension": "1339", 71 | "city": "Los Andes", 72 | "start_date": "2011/26/01" 73 | }, 74 | { 75 | "name": "Armand Suarez", 76 | "extension": "6583", 77 | "city": "Funtua", 78 | "start_date": "1999/06/11" 79 | }, 80 | { 81 | "name": "Gretchen Rogers", 82 | "extension": "5393", 83 | "city": "Moxhe", 84 | "start_date": "1998/26/10" 85 | }, 86 | { 87 | "name": "Harding Thompson", 88 | "extension": "2824", 89 | "city": "Abeokuta", 90 | "start_date": "2008/06/08" 91 | }, 92 | { 93 | "name": "Mira Rocha", 94 | "extension": "4393", 95 | "city": "Port Harcourt", 96 | "start_date": "2002/04/10" 97 | }, 98 | { 99 | "name": "Drew Phillips", 100 | "extension": "2931", 101 | "city": "Goes", 102 | "start_date": "2011/18/10" 103 | }, 104 | { 105 | "name": "Emerald Warner", 106 | "extension": "6205", 107 | "city": "Chiavari", 108 | "start_date": "2002/08/04" 109 | }, 110 | { 111 | "name": "Colin Burch", 112 | "extension": "7457", 113 | "city": "Anamur", 114 | "start_date": "2004/02/01" 115 | }, 116 | { 117 | "name": "Russell Haynes", 118 | "extension": "8916", 119 | "city": "Frascati", 120 | "start_date": "2015/28/04" 121 | }, 122 | { 123 | "name": "Brennan Brooks", 124 | "extension": "9011", 125 | "city": "Olmué", 126 | "start_date": "2000/18/04" 127 | }, 128 | { 129 | "name": "Kane Anthony", 130 | "extension": "8075", 131 | "city": "LaSalle", 132 | "start_date": "2006/21/05" 133 | }, 134 | { 135 | "name": "Scarlett Hurst", 136 | "extension": "1019", 137 | "city": "Brampton", 138 | "start_date": "2015/07/01" 139 | }, 140 | { 141 | "name": "James Scott", 142 | "extension": "3008", 143 | "city": "Meux", 144 | "start_date": "2007/30/05" 145 | }, 146 | { 147 | "name": "Desiree Ferguson", 148 | "extension": "9054", 149 | "city": "Gojra", 150 | "start_date": "2009/15/02" 151 | }, 152 | { 153 | "name": "Elaine Bishop", 154 | "extension": "9160", 155 | "city": "Petrópolis", 156 | "start_date": "2008/23/12" 157 | }, 158 | { 159 | "name": "Hilda Nelson", 160 | "extension": "6307", 161 | "city": "Posina", 162 | "start_date": "2004/23/05" 163 | }, 164 | { 165 | "name": "Evangeline Beasley", 166 | "extension": "3820", 167 | "city": "Caplan", 168 | "start_date": "2009/12/03" 169 | }, 170 | { 171 | "name": "Wyatt Riley", 172 | "extension": "5694", 173 | "city": "Cavaion Veronese", 174 | "start_date": "2012/19/02" 175 | }, 176 | { 177 | "name": "Wyatt Mccarthy", 178 | "extension": "3547", 179 | "city": "Patan", 180 | "start_date": "2014/23/06" 181 | }, 182 | { 183 | "name": "Cairo Rice", 184 | "extension": "6273", 185 | "city": "Ostra Vetere", 186 | "start_date": "2016/27/02" 187 | }, 188 | { 189 | "name": "Sylvia Peters", 190 | "extension": "6829", 191 | "city": "Arrah", 192 | "start_date": "2015/03/02" 193 | }, 194 | { 195 | "name": "Kasper Craig", 196 | "extension": "5515", 197 | "city": "Firenze", 198 | "start_date": "2015/26/04" 199 | }, 200 | { 201 | "name": "Leigh Ruiz", 202 | "extension": "5112", 203 | "city": "Lac Ste. Anne", 204 | "start_date": "2001/09/02" 205 | }, 206 | { 207 | "name": "Athena Aguirre", 208 | "extension": "5741", 209 | "city": "Romeral", 210 | "start_date": "2010/24/03" 211 | }, 212 | { 213 | "name": "Riley Nunez", 214 | "extension": "5533", 215 | "city": "Sart-Eustache", 216 | "start_date": "2003/26/02" 217 | }, 218 | { 219 | "name": "Lois Talley", 220 | "extension": "9393", 221 | "city": "Dorchester", 222 | "start_date": "2014/05/01" 223 | }, 224 | { 225 | "name": "Hop Bass", 226 | "extension": "1024", 227 | "city": "Westerlo", 228 | "start_date": "2012/25/09" 229 | }, 230 | { 231 | "name": "Kalia Diaz", 232 | "extension": "9184", 233 | "city": "Ichalkaranji", 234 | "start_date": "2013/26/06" 235 | }, 236 | { 237 | "name": "Maia Pate", 238 | "extension": "6682", 239 | "city": "Louvain-la-Neuve", 240 | "start_date": "2011/23/04" 241 | }, 242 | { 243 | "name": "Macaulay Pruitt", 244 | "extension": "4457", 245 | "city": "Fraser-Fort George", 246 | "start_date": "2015/03/08" 247 | }, 248 | { 249 | "name": "Danielle Oconnor", 250 | "extension": "9464", 251 | "city": "Neuwied", 252 | "start_date": "2001/05/10" 253 | }, 254 | { 255 | "name": "Kato Carr", 256 | "extension": "4842", 257 | "city": "Faridabad", 258 | "start_date": "2012/11/05" 259 | }, 260 | { 261 | "name": "Malachi Mejia", 262 | "extension": "7133", 263 | "city": "Vorst", 264 | "start_date": "2007/25/04" 265 | }, 266 | { 267 | "name": "Dominic Carver", 268 | "extension": "3476", 269 | "city": "Pointe-aux-Trembles", 270 | "start_date": "2014/14/03" 271 | }, 272 | { 273 | "name": "Paki Santos", 274 | "extension": "4424", 275 | "city": "Cache Creek", 276 | "start_date": "2001/18/11" 277 | }, 278 | { 279 | "name": "Ross Hodges", 280 | "extension": "1862", 281 | "city": "Trazegnies", 282 | "start_date": "2010/19/09" 283 | }, 284 | { 285 | "name": "Hilda Whitley", 286 | "extension": "3514", 287 | "city": "New Sarepta", 288 | "start_date": "2011/05/07" 289 | }, 290 | { 291 | "name": "Roth Cherry", 292 | "extension": "4006", 293 | "city": "Flin Flon", 294 | "start_date": "2008/02/09" 295 | } 296 | ] 297 | -------------------------------------------------------------------------------- /docs/demos/28-sort-order-attribute/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sort order attribute - simple-datatables 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

16 | simple-datatables 17 |

18 | Documentation 19 | Demos 20 |
21 | 22 |

Sort order attribute

23 |

Try sorting the two table columns. Then look at the source of the page. The data-order attributes on the cells take precedent over the contents of the cells.

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
NameVariation
A2000.9%
B100.3%
C20%
D-7%
E-9%
54 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/demos/30-custom-search-method/script.js: -------------------------------------------------------------------------------- 1 | import {DataTable} from "../dist/module.js" 2 | const datatable = new DataTable("#demo-table", { 3 | perPageSelect: [5, 10, 15, ["All", -1]], 4 | columns: [ 5 | { 6 | select: 1, 7 | sortable: false, 8 | searchMethod: (terms, cell, row, _column, source) => { 9 | let found 10 | if (source === "class-filter") { 11 | found = terms.find( 12 | // For each of the terms, check if at least one of the class tags in the cell exactly matches the term 13 | term => cell.data.find(span => span.attributes?.class === `class ${term}`) 14 | ) 15 | } else { 16 | // This is normal search. We just check if one of the terms is part of the text of the cell 17 | found = terms.find( 18 | term => cell.text.toLowerCase().includes(term.toLowerCase().trim()) 19 | ) 20 | } 21 | if (found) return true 22 | return false 23 | } 24 | }, 25 | { 26 | select: 4, 27 | searchMethod: (terms, cell, row, _column, source) => { 28 | if (source === "completion-filter") { 29 | const cellPercentage = parseInt(cell.data, 10) 30 | const minPercentage = parseInt(terms[0], 10) 31 | if (cellPercentage >= minPercentage) return true 32 | return false 33 | } 34 | // This is normal search. We just check if one of the terms is part of the text of the cell 35 | const found = terms.find( 36 | term => cell.data.includes(term.toLowerCase().trim()) 37 | ) 38 | 39 | if (found) return true 40 | return false 41 | } 42 | } 43 | ] 44 | }) 45 | 46 | 47 | // Multi-select dropdown 48 | let isDropdownOpen = false 49 | 50 | const toggleDropdown = () => { 51 | const checkboxes = document.getElementById("checkboxes") 52 | if (isDropdownOpen) { 53 | checkboxes.style.display = "none" 54 | isDropdownOpen = false 55 | } else { 56 | checkboxes.style.display = "block" 57 | isDropdownOpen = true 58 | } 59 | } 60 | 61 | document.querySelector(".select-box").addEventListener("click", toggleDropdown) 62 | // Close the dropdown if the user clicks outside of it 63 | window.onclick = function(event) { 64 | if (!event.target.matches(".select-box") && !event.target.matches(".over-select") && !event.target.closest("#checkboxes")) { 65 | const checkboxes = document.getElementById("checkboxes") 66 | checkboxes.style.display = "none" 67 | isDropdownOpen = false 68 | } 69 | if (event.target.closest("#checkboxes label")) { 70 | // Change in checked classes, restart search 71 | // Get all checked checkboxes 72 | const checked = Array.from(document.querySelectorAll("#checkboxes input:checked")).map(checkbox => checkbox.value) 73 | if (!checked.length) { 74 | // Don't allow deselecting the last checkbox. 75 | event.target.closest("#checkboxes label").querySelector("input").checked = true 76 | return 77 | } 78 | datatable.multiSearch([ 79 | {terms: checked, 80 | columns: [1]} 81 | ], "class-filter") 82 | } 83 | 84 | } 85 | 86 | const updateSliderValue = value => { 87 | document.getElementById("slider-value").innerHTML = `${value}%` 88 | datatable.multiSearch([ 89 | {terms: [value], 90 | columns: [4]} 91 | ], "completion-filter") 92 | } 93 | 94 | document.querySelector("#percentage-slider").addEventListener("input", function() { 95 | updateSliderValue(this.value) 96 | }) 97 | 98 | 99 | window.dt = datatable 100 | -------------------------------------------------------------------------------- /docs/demos/30-custom-search-method/style.css: -------------------------------------------------------------------------------- 1 | span.class { 2 | display: inline-block; 3 | padding: 5px 10px; 4 | margin: 3px; 5 | border-radius: 12px; 6 | font-size: 14px; 7 | font-weight: 500; 8 | color: white; 9 | } 10 | 11 | /* Pastel colors for different subjects */ 12 | .math { 13 | background-color: #a3d8f4; /* pastel blue */ 14 | color: #1d4e89; 15 | } 16 | 17 | .history { 18 | background-color: #f9c6c9; /* pastel pink */ 19 | color: #87373f; 20 | } 21 | 22 | .english { 23 | background-color: #c6e1bc; /* pastel green */ 24 | color: #2b5d2e; 25 | } 26 | 27 | .physics { 28 | background-color: #f6d8ac; /* pastel yellow */ 29 | color: #8a5e11; 30 | } 31 | 32 | .chemistry { 33 | background-color: #f3bbd3; /* pastel magenta */ 34 | color: #73234a; 35 | } 36 | 37 | .biology { 38 | background-color: #b7e0e5; /* pastel teal */ 39 | color: #1b5b5e; 40 | } 41 | 42 | .french { 43 | background-color: #ecd9f2; /* pastel purple */ 44 | color: #562e6d; 45 | } 46 | 47 | .german { 48 | background-color: #f2d3a1; /* pastel orange */ 49 | color: #9b6c22; 50 | } 51 | 52 | /* Style for the dropdown and the checkboxes */ 53 | .multiselect { 54 | position: relative; 55 | width: 200px; 56 | } 57 | 58 | .select-box { 59 | position: relative; 60 | display: block; 61 | border: 1px solid #aaa; 62 | padding: 8px; 63 | background-color: #fff; 64 | cursor: pointer; 65 | } 66 | 67 | .select-box select { 68 | width: 100%; 69 | border: none; 70 | outline: none; 71 | background: none; 72 | font-size: 16px; 73 | } 74 | 75 | .over-select { 76 | position: absolute; 77 | left: 0; 78 | right: 0; 79 | top: 0; 80 | bottom: 0; 81 | cursor: pointer; 82 | } 83 | 84 | .checkboxes { 85 | display: none; 86 | border: 1px solid #aaa; 87 | padding: 10px; 88 | background-color: #f9f9f9; 89 | position: absolute; 90 | z-index: 1; 91 | width: 100%; 92 | } 93 | 94 | .checkboxes label { 95 | display: block; 96 | margin-bottom: 5px; 97 | cursor: pointer; 98 | } 99 | 100 | .checkboxes input[type="checkbox"] { 101 | margin-right: 10px; 102 | } 103 | 104 | /* Additional styling */ 105 | .select-box:hover { 106 | background-color: #e9e9e9; 107 | } 108 | -------------------------------------------------------------------------------- /docs/demos/5-column-manipulation/datatable.column.json: -------------------------------------------------------------------------------- 1 | {"heading":"Completion","data":[37,97,63,30,17,57,93,100,44,33,77,49,9,24,10,14,58,58,34,18,2,93,94,55,81,48,76,62,67,9,13,13,56,28,15,30,51,85,92,50,92,17,96,26,71,82,87,94,8,21,52,72,6,58,70,59,88,79,36,71,89,98,88,8,22,16,41,16,23,65,61,24,90,37,41,2,72,23,94,5,50,2,74,53,51,48,25,93,65,86,72,20,41,47,24,68,4,16,39,31]} 2 | -------------------------------------------------------------------------------- /docs/demos/5-column-manipulation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Column manipulation - simple-datatables 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

16 | simple-datatables 17 |

18 | Documentation 19 | Demos 20 |
21 | 22 |

Column manipulation

23 | 24 |
25 | 26 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /docs/demos/demo.css: -------------------------------------------------------------------------------- 1 | * { margin: 0; padding: 0; box-sizing: border-box; font-family: Roboto, Arial, Helvetica, sans-serif; } 2 | html, body { overflow-x: hidden; overflow-y: auto; } 3 | body { margin: 2vw 5vw; } 4 | header { display: flex; align-items: baseline; gap: 1rem; } 5 | header h1 a { color: #16378d; } 6 | nav { display: flex; flex-direction: column; } 7 | a { text-decoration: none; color: #0031e6; } 8 | a:hover, a:focus { color: #00198b; } 9 | h2 { margin: 1rem 0; } 10 | button:not(.datatable-sorter):not(.datatable-filter) { cursor: pointer; padding: 0.5em; } 11 | 12 | tbody button { background: #565656; color: white; border: none; border-radius: 4px; padding: .25rem .5rem; margin: 0 0 0 .25rem; } 13 | tbody button:hover, tbody button:focus { background: #3b3b3b; } 14 | -------------------------------------------------------------------------------- /docs/demos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demos - simple-datatables 7 | 8 | 9 | 10 | 11 |
12 |

13 | simple-datatables 14 |

15 | Documentation 16 | Demos 17 |
18 |

Demos

19 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/documentation/API.md: -------------------------------------------------------------------------------- 1 | >Please note that the API is not finalised and may change so check back once in while. 2 | 3 | ### dom 4 | #### type `Object` 5 | 6 | Returns a reference to the [`HTMLTableElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLTableElement). 7 | 8 | --- 9 | 10 | ### wrapper 11 | #### type `Object` 12 | 13 | Returns a reference to the main `HTMLDivElement` that wraps the entire layout. 14 | 15 | --- 16 | 17 | ### container 18 | #### type `Object` 19 | 20 | Returns a reference to the main `HTMLDivElement` that contains the table. 21 | 22 | --- 23 | 24 | ### pagers 25 | #### type `Array` 26 | 27 | Returns a reference to the currently displayed pagers. 28 | 29 | --- 30 | 31 | ### options 32 | #### type `Object` 33 | 34 | Returns the current configuration options. See [Options](Options) 35 | 36 | --- 37 | 38 | ### initialized 39 | #### type `Boolean` 40 | 41 | Returns true if the library is fully loaded and all HTML is rendered. 42 | 43 | --- 44 | 45 | ### data 46 | #### type `Object` 47 | 48 | The data of the table, containing two parts: `headings` and `data` (contents of the tables). 49 | 50 | * `headings`: An array of **header cells**. 51 | 52 | * `data`: An array of rows of data. Each row consists of an array of **row cells**. 53 | 54 | 55 | **Header cells** are objects with these fields: 56 | 57 | * `data` (any, required): The headers data in it's original format. 58 | 59 | * `text` (string, optional): In case the browser's automatic conversion of the data field to a string to display in a browser is not working correctly, this field can be sued to control what is being rendered. 60 | 61 | * `type` (string, optional): "html" in case of the data field is an array of DiffDOM nodes. "string" in case of a plaintext string. 62 | 63 | **Row cells** are objects with these fields: 64 | 65 | * `attributes` (any, optional): The row attributes. Use like `{ "class": "my-class", "style": "background-color: pink;" }`. 66 | 67 | * `cells` (any[], required): The list of cells in this row. See "Content cells" for more details. 68 | 69 | **Content cells** are also objects with the same `data` and `text` fields that **header fields** has and additionally with this field: 70 | 71 | * `order` (string or integer, optional): Used for sorting in case and is useful if the `data` field cannot be used for sorting. 72 | 73 | --- 74 | 75 | ### data-index 76 | #### type `Integer` 77 | 78 | All rows in the `data.data` array have a custom propery named `data-index`. This represents the position in the `data` array. It can be useful for getting the correct position of a row as the native `rowIndex` property may be either `-1` if the row isn't rendered or incorrect if you're on any other page than page 1. 79 | 80 | Also, in some browsers, the first row of a `tbody` element will have a `rowIndex` of `1` instead of `0` as they take the `thead` row as the first row. 81 | 82 | For example if you want to remove the first row on page 5 while showing 5 rows per page (21st row): 83 | 84 | ```javascript 85 | // grab the first row on page 5 86 | let firstRow = document.querySelector("tbody tr"); 87 | 88 | // INCORRECT: Because it's the first rendered row the native firstRow.rowIndex 89 | // will be 1 which will remove the second row in the data array 90 | datatable.rows.remove(firstRow.rowIndex); 91 | 92 | // CORRECT: firstRow.dataset.index will return 20 which is the 93 | // correct position (21st row) in the data array 94 | datatable.rows.remove(parseInt(firstRow.dataset.index)); 95 | 96 | ``` 97 | 98 | --- 99 | 100 | ### pages 101 | #### type `Array` 102 | 103 | Returns a collection of pages each of which contain collections of `HTMLTableRowElement`s. 104 | 105 | --- 106 | 107 | ### hasRows 108 | #### type `Boolean` 109 | 110 | Returns `true` if the current table has rows. 111 | 112 | --- 113 | 114 | ### hasHeadings 115 | #### type `Boolean` 116 | 117 | Returns `true` if the current table has headings. 118 | 119 | --- 120 | 121 | ### currentPage 122 | #### type `Integer` 123 | 124 | Returns the current page number. 125 | 126 | --- 127 | 128 | ### totalPages 129 | #### type `Integer` 130 | 131 | Returns the number of pages. 132 | 133 | --- 134 | 135 | ### onFirstPage 136 | #### type `Boolean` 137 | 138 | Returns `true` if the current page is also the first page. 139 | 140 | --- 141 | 142 | ### onLastPage 143 | #### type `Boolean` 144 | 145 | Returns `true` if the current page is also the last page. 146 | 147 | --- 148 | 149 | ### searching 150 | #### type `Boolean` 151 | 152 | Returns `true` if a search is currently being done and search results are displayed. 153 | 154 | --- 155 | 156 | ### searchData 157 | #### type `Array` 158 | 159 | Returns an array of index numbers of current search result rows from `data.data`. 160 | 161 | --- 162 | -------------------------------------------------------------------------------- /docs/documentation/Adding-a-column-from-a-remote-source.md: -------------------------------------------------------------------------------- 1 | Adding a column from a remote source (AJAX) is simple with the `columns` API. 2 | 3 | Let's say you've selected a column from your MySQL table and you want to include it in your `datatable` instance. You can encode the column data as a `JSON string` and fetch it: 4 | ```javascript 5 | { 6 | heading: "Progress" 7 | data: [ 8 | "37%", 9 | "97%", 10 | "63%", 11 | "30%", 12 | ... 13 | ] 14 | } 15 | ``` 16 | 17 | 18 | ```javascript 19 | let addNewColumn = function() { 20 | let columnData = "remote/data/url" 21 | 22 | fetch(columnData) 23 | .then(response => response.json()) 24 | .then(data => datatable.columns.add(data)) 25 | } 26 | ``` -------------------------------------------------------------------------------- /docs/documentation/Dynamically-adding-data.md: -------------------------------------------------------------------------------- 1 | New data of many formats can be added at any time with the `insert()` method as well as the `rows` and `columns` API. 2 | 3 | --- 4 | 5 | You can quickly add a new row with an `array` of cell data: 6 | 7 | ```javascript 8 | let newRow = ["Cell 1", "Cell 2", "Cell 3", "Cell 4", ...]; 9 | 10 | datatable.rows.add(newRow); 11 | ``` 12 | 13 | The `add()` method also accepts a nested `array` for adding multiple rows: 14 | ```javascript 15 | let newRows = [ 16 | ["Cell 1", "Cell 2", "Cell 3", "Cell 4", ...], 17 | ["Cell 1", "Cell 2", "Cell 3", "Cell 4", ...], 18 | ["Cell 1", "Cell 2", "Cell 3", "Cell 4", ...], 19 | ["Cell 1", "Cell 2", "Cell 3", "Cell 4", ...], 20 | ... 21 | ]; 22 | 23 | datatable.insert({ data: newRows }); 24 | ``` 25 | --- 26 | 27 | The `insert()` method can accept both an `object` or and array of `key-value objects` depending on your setup: 28 | 29 | ```javascript 30 | 31 | let newData = { 32 | headings: ["Heading 1", "Heading 2", "Heading 3", "Heading 4", ...], 33 | data: [ 34 | ["Cell 1", "Cell 2", "Cell 3", "Cell 4", ...], 35 | ["Cell 5", "Cell 6", "Cell 7", "Cell 8", ...], 36 | ["Cell 9", "Cell 10", "Cell 11", "Cell 12", ...], 37 | ... 38 | ] 39 | }; 40 | 41 | // or 42 | 43 | let newData = [ 44 | { 45 | "Heading 1": "Cell 1", 46 | "Heading 2": "Cell 2", 47 | "Heading 3": "Cell 3", 48 | "Heading 4": "Cell 4", 49 | }, 50 | { 51 | "Heading 1": "Cell 5", 52 | "Heading 2": "Cell 6", 53 | "Heading 3": "Cell 7", 54 | "Heading 4": "Cell 8", 55 | } 56 | ]; 57 | 58 | // Insert the data 59 | datatable.insert(newData); 60 | 61 | ``` 62 | 63 | Note that while the `key-value` method doesn't require you to order the data correctly to match the table layout, the instance will still check that the given `key` (heading) is present and will skip the insertion if it isn't, so make sure the `keys` match the column labels. 64 | 65 | --- 66 | 67 | `JSON` strings can easily be imported also: 68 | 69 | ```json 70 | "[{ 71 | 'Heading 1': 'Value 1', 72 | 'Heading 2': 'Value 2', 73 | 'Heading 3': 'Value 3', 74 | ... 75 | }, 76 | { 77 | 'Heading 1': 'Value 4', 78 | 'Heading 2': 'Value 5', 79 | 'Heading 3': 'Value 6', 80 | ... 81 | }]" 82 | ``` 83 | 84 | ```javascript 85 | import { 86 | DataTable, 87 | convertJSON 88 | } from "simple-datatables" 89 | const dataTable = new DataTable("#myTable") 90 | const convertedData = convertJSON({ 91 | data: // the above JSON string 92 | }) 93 | dataTable.insert(convertedData) 94 | ``` 95 | 96 | or `csv` strings: 97 | 98 | ```text 99 | Name,Ext.,City,Start Date 100 | Hammett Gordon,8101,Wah,1998/06/09 101 | Kyra Moses,3796,Quenast,1998/07/07 102 | Kelly Cameron,4836,Fontaine-Valmont,1999/02/07 103 | Theodore Duran,8971,Dhanbad,1999/04/07 104 | ... 105 | ``` 106 | 107 | ```javascript 108 | import { 109 | DataTable, 110 | convertCSV 111 | } from "simple-datatables" 112 | const dataTable = new DataTable("#myTable") 113 | const convertedData = convertJSON({ 114 | data: // the above CSV string, 115 | headings: true, 116 | columnDelimiter: ",", 117 | lineDelimiter: "\n" 118 | }) 119 | dataTable.insert(convertedData) 120 | ``` 121 | -------------------------------------------------------------------------------- /docs/documentation/Events.md: -------------------------------------------------------------------------------- 1 | simple-datatables fires its own events which you can listen for by utilising the `.on()` method: 2 | 3 | ```javascript 4 | let dataTable = new DataTable(myTable); 5 | 6 | dataTable.on('datatable.XXXX', function(args) { 7 | // Do something when datatable.XXXX fires 8 | }); 9 | ``` 10 | 11 | ### `datatable.init` 12 | Fires when the table is fully rendered and ready for use. 13 | 14 | ### `datatable.page` 15 | Fires on page change. 16 | 17 | A single argument is available which returns the page number: 18 | 19 | ```javascript 20 | dataTable.on('datatable.page', function(page) { 21 | // 22 | }); 23 | ``` 24 | 25 | ### `datatable.page:before` 26 | Fires before page change. 27 | 28 | A single argument is available which returns the page number: 29 | 30 | ```javascript 31 | dataTable.on('datatable.page:before', function(page) { 32 | // 33 | }); 34 | ``` 35 | 36 | ### `datatable.perpage` 37 | Fires when the perPage option is changed with the dropdown. A single argument returns the per-page value: 38 | 39 | ```javascript 40 | dataTable.on('datatable.perpage', function(perpage) { 41 | // 42 | }); 43 | ``` 44 | 45 | ### `datatable.perpage:before` 46 | Fires before the perPage option is changed with the dropdown. A single argument returns the per-page value: 47 | 48 | ```javascript 49 | dataTable.on('datatable.perpage:before', function(perpage) { 50 | // 51 | }); 52 | ``` 53 | 54 | ### `datatable.refresh` 55 | Fires when the `.refresh()` method is called. 56 | 57 | ### `datatable.refresh:before` 58 | Fires before the `.refresh()` method is called. 59 | 60 | ### `datatable.search` 61 | Fires on keyup during a search. 62 | 63 | Two arguments are available: `query` which returns the query string entered and `matched` which returns an array of 64 | rows containing the matched string: 65 | 66 | ```javascript 67 | dataTable.on('datatable.search', function(query, matched) { 68 | // 69 | }); 70 | ``` 71 | 72 | ### `datatable.search:before` 73 | Fires on keyup before a search. 74 | 75 | Two arguments are available: `query` which returns the query string entered and `matched` which returns an array of rows containing the matched string: 76 | 77 | ```javascript 78 | dataTable.on('datatable.search:before', function(query, matched) { 79 | // 80 | }); 81 | ``` 82 | 83 | ### `datatable.selectrow` 84 | Fires when user selects a row - either by mouse click on a row or by a key specified by [rowSelectionKeys](rowSelectionKeys) during keyboard based navigation (requires option 85 | [rowNavigation](rowNavigation)). 86 | 87 | Three arguments are available: 88 | 89 | * `row` which returns the `` element that was selected, 90 | * `event` which returns the event that caused the selection, 91 | * `focused` (true/false) which returns whether the table had focus when the row was selected. 92 | 93 | 94 | You can run `event.preventDefault()` like this: 95 | 96 | ```javascript 97 | dataTable.on("datatable.selectrow", (rowIndex, event, focused) => { 98 | event.preventDefault(); 99 | ... 100 | }); 101 | ``` 102 | 103 | See the [row navigation demo](../demos/14-row-navigation/index.html) for a more complete example. 104 | 105 | ### `datatable.sort` 106 | Fires when the table is sorted. 107 | 108 | Two arguments are available when listening for this event: `column` which returns the column index and `direction` which returns the sort direction: 109 | 110 | ```javascript 111 | dataTable.on('datatable.sort', function(column, direction) { 112 | // 113 | }); 114 | ``` 115 | 116 | ### `datatable.update` 117 | Fires when the `.update()` method is called. 118 | 119 | ### `datatable.update:before` 120 | Fires before the `.update()` method is called. 121 | 122 | ### `editable.init` 123 | Fires when the editor is initialized. 124 | 125 | ### `editable.save.cell` 126 | Fires when the cell is saved (even when the value is not actually updated). 127 | 128 | ### `editable.save.row` 129 | Fires when the row is saved. 130 | 131 | ### `editable.context.open` 132 | Fires when the context menu is opened. 133 | 134 | ### `editable.context.close` 135 | Fires when the context menu is closed. 136 | -------------------------------------------------------------------------------- /docs/documentation/Getting-Started.md: -------------------------------------------------------------------------------- 1 | ### Install 2 | 3 | ``` 4 | npm install simple-datatables --save 5 | ``` 6 | 7 | --- 8 | 9 | ### Initialise 10 | 11 | The JS file should be included before any scripts that call the plugin. 12 | 13 | Then just initialise the plugin by either passing a reference to the table or a CSS3 selector string as the first parameter: 14 | 15 | ```javascript 16 | let myTable = document.querySelector("#myTable"); 17 | let dataTable = new DataTable(myTable); 18 | 19 | // or 20 | 21 | let dataTable = new DataTable("#myTable"); 22 | 23 | ``` 24 | 25 | You can also pass the options object as the second parameter: 26 | 27 | ```javascript 28 | let dataTable = new DataTable("#myTable", { 29 | searchable: false, 30 | fixedHeight: true, 31 | ... 32 | }); 33 | ``` 34 | 35 | ### Initial data 36 | 37 | You can either supply initial data through the options object or by starting with a table that already has data filled in. 38 | 39 | If you start out with a table that already contains header cells, you can add these attributes to individual header cells 40 | to influence how the corresponding column is treated: 41 | 42 | * `data-type`: Can be used to set the type of the column. It works the same as using `type` in the [columns](columns) option. 43 | 44 | * `data-hidden`: If set to `"true"` will hide the column. It works the same as using `hidden` in the [columns](columns) option. 45 | 46 | * `data-searchable`: If set to `"false"` will prevent searching of the column. It works the same as using `searchable` in the [columns](columns) option. 47 | 48 | * `data-sortable`: If set to `"false"` will prevent sorting of the column. It works the same as using `sortable` in the [columns](columns) option. 49 | 50 | If you start out with a table that already contains data, you can add these attributes to individual cells to influence how 51 | the cell is being processed: 52 | 53 | * `data-content`: If this attribute is present on a cell, the value of this attribute rather than the contents of the cell 54 | will be processed. 55 | 56 | * `data-order`: If this attribute is present on a cell, the value of this attribute will be used for ordering the row in case 57 | of sorting. Any other sort order will be overridden. IF the field only contains numeric characters, the field will first 58 | be converted to a number. 59 | -------------------------------------------------------------------------------- /docs/documentation/Options.md: -------------------------------------------------------------------------------- 1 | simple-datatables can be initialised with custom options passed as the second parameter of the constructor. 2 | 3 | ```javascript 4 | let options = { 5 | searchable: true, 6 | perPage: 10, 7 | ... 8 | }; 9 | let dataTable = new DataTable(myTable, options); 10 | ``` 11 | 12 | --- 13 | ### Data 14 | * [data](data) 15 | * [type](columns#type) 16 | * [format](columns#format) 17 | ### Appearance 18 | * [caption](caption) 19 | * [classes](classes) 20 | * [columns](columns) 21 | * [fixedColumns](fixedColumns) 22 | * [fixedHeight](fixedHeight) 23 | * [footer](footer) 24 | * [header](header) 25 | * [hiddenHeader](hiddenHeader) 26 | * [labels](labels) 27 | * [template](template) 28 | * [scrollY](scrollY) 29 | ### Rendering 30 | * [pagerRender](pagerRender) 31 | * [rowRender](rowRender) 32 | * [tableRender](tableRender) 33 | * [diffDomOptions](diffDomOptions) 34 | ### Pagination 35 | * [paging](paging) 36 | * [perPage](perPage) 37 | * [perPageSelect](perPageSelect) 38 | * [firstLast](firstLast) 39 | * [nextPrev](nextPrev) 40 | * [firstText](firstText) 41 | * [lastText](lastText) 42 | * [prevText](prevText) 43 | * [nextText](nextText) 44 | * [ellipsisText](ellipsisText) 45 | * [truncatePager](truncatePager) 46 | * [pagerDelta](pagerDelta) 47 | ### Searching 48 | * [searchable](searchable) 49 | * [sensitivity](columns#sensitivity) 50 | * [searchQuerySeparator](searchQuerySeparator) 51 | * [searchMethod](searchMethod) 52 | ### Sorting 53 | * [sortable](sortable) 54 | * [locale](columns#locale) 55 | * [numeric](columns#numeric) 56 | * [caseFirst](columns#caseFirst) 57 | * [ignorePunctuation](columns#ignorePunctuation) 58 | ### Navigation 59 | * [rowNavigation](rowNavigation) 60 | * [rowSelectionKeys](rowSelectionKeys) 61 | * [tabIndex](tabIndex) 62 | ### Other 63 | * [destroyable](destroyable) 64 | -------------------------------------------------------------------------------- /docs/documentation/caption.md: -------------------------------------------------------------------------------- 1 | ### `caption` 2 | #### Type: `string` 3 | #### Default: `undefined` 4 | 5 | Display a table caption with [](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption). No caption 6 | is displayed by default. 7 | -------------------------------------------------------------------------------- /docs/documentation/classes.md: -------------------------------------------------------------------------------- 1 | ### `classes` 2 | #### Type: `object` 3 | 4 | ### Default 5 | ```javascript 6 | { 7 | active: "datatable-active", 8 | bottom: "datatable-bottom", 9 | container: "datatable-container", 10 | cursor: "datatable-cursor", 11 | dropdown: "datatable-dropdown", 12 | ellipsis: "datatable-ellipsis", 13 | empty: "datatable-empty", 14 | headercontainer: "datatable-headercontainer", 15 | info: "datatable-info", 16 | input: "datatable-input", 17 | loading: "datatable-loading", 18 | pagination: "datatable-pagination", 19 | paginationList: "datatable-pagination-list", 20 | search: "datatable-search", 21 | selector: "datatable-selector", 22 | sorter: "datatable-sorter", 23 | table: "datatable-table", 24 | top: "datatable-top", 25 | wrapper: "datatable-wrapper" 26 | }, 27 | ``` 28 | 29 | Allows for customizing the classnames used by simple-datatables. 30 | 31 | Please note that class names cannot be empty and cannot be reused between different attributes. This is required for simple-datatables to work correctly. 32 | 33 | Multiple classes can be provided per attribute. Please make sure that classes are be separated by spaces. 34 | For example, `dt.options.classes.table = "first second"` will apply classes `first` and `second` to the generated table. -------------------------------------------------------------------------------- /docs/documentation/columns-API.md: -------------------------------------------------------------------------------- 1 | As of `v1.2.0`, the `columns` API is implemented and allows access to the table columns for quick manipulation. 2 | 3 | As of `v4.0.0`, `columns` is a property and not a method on the current instance. 4 | 5 | To use the `columns` API just access under the `columns` property of the current instance: 6 | 7 | ```javascript 8 | let columns = datatable.columns; 9 | ``` 10 | 11 | You can then chain the following methods. 12 | 13 | --- 14 | 15 | ### `get(column [integer])` 16 | 17 | Fetch read-only data about the column at index `column`. The `column` parameter should be an integer representing the column. 18 | 19 | --- 20 | 21 | ### `size()` 22 | 23 | Fetch the number of columns. 24 | 25 | --- 26 | 27 | ### `sort(column [integer], direction [string])` 28 | 29 | Sort the selected column. The `column` parameter should be an integer representing the column, starting with zero for the first column. The `direction` parameter is optional. 30 | 31 | --- 32 | 33 | ### `add(data [object])` 34 | 35 | Add a new column to the current instance. The `data` parameter should be an object with the required `heading` and `data` properties set. The `heading` property should be a `string` representing the new column's heading. The `data` property should be an array of `strings` representing the cell content of the new column. 36 | 37 | ```javascript 38 | let columns = datatable.columns; 39 | 40 | let newData = { 41 | heading: "Column Heading", 42 | data: [ 43 | "Value 1", 44 | "Value 2", 45 | "Value 3", 46 | ... 47 | ] 48 | }; 49 | 50 | // Add the new column 51 | columns.add(newData); 52 | ``` 53 | 54 | You can also pass the `sortable`, `type` and `format` properties to further customise the new column. 55 | 56 | The `sortable` property defaults to `true`, unless sorting is disabled globally. 57 | 58 | ```javascript 59 | let newData = { 60 | type: "date", 61 | format: "YYYY/MM/DD" 62 | heading: "Start Date", 63 | data: [ 64 | "1999/10/25", 65 | "2000/05/12", 66 | "2003/08/01", 67 | ... 68 | ] 69 | }; 70 | ``` 71 | 72 | --- 73 | 74 | ### `remove(select [integer|array])` 75 | 76 | Remove a column or columns from the current instance. The `select` parameter should be either an `integer` or an array of `integers` representing the column indexes to be removed. 77 | 78 | ```javascript 79 | let columns = datatable.columns; 80 | 81 | // Remove the 4th column 82 | columns.remove(3); 83 | 84 | // Remove the 1st and 2nd column 85 | columns.remove([0, 1]); 86 | 87 | // Remove the last column 88 | columns.remove(datatable.headings.length - 1); 89 | ``` 90 | 91 | --- 92 | 93 | ### `hide(select [integer|array])` 94 | 95 | Hides the selected column(s). The columns will not be visible and will be omitted from search results and exported data. 96 | 97 | ```javascript 98 | // Hide the first and second columns 99 | columns.hide([0, 1]); 100 | ``` 101 | 102 | --- 103 | 104 | ### `show(select [integer|array])` 105 | 106 | Shows the selected column(s) (if hidden). The columns will be visible and will be included in search results and exported data. 107 | 108 | ```javascript 109 | // Show the first and second columns 110 | columns.show([0, 1]); 111 | ``` 112 | 113 | --- 114 | 115 | ### `visible(select [integer|array])` 116 | 117 | Checks to see if the selected column(s) are visible. Returns a `boolean` for single indexes or an `array` of `boolean`s for multiple indexes. 118 | 119 | If you omit the `select` parameter, an `array` of `booleans` will be returned representing all available columns. 120 | 121 | ```javascript 122 | let columns = datatable.columns; 123 | 124 | // Hide the 4th of 5 columns 125 | columns.hide(3); 126 | 127 | // Check visiblilty 128 | columns.visible(3); // returns false 129 | ``` 130 | 131 | or 132 | 133 | ```javascript 134 | columns.visible([0, 1, 2, 3]); // returns [true, true, true, false] 135 | ``` 136 | 137 | or 138 | 139 | ```javascript 140 | columns.visible(); // returns [true, true, true, false, true] 141 | ``` 142 | 143 | --- 144 | 145 | ### `hidden(select [integer|array])` 146 | 147 | Checks to see if the selected column(s) are visible. Returns a `boolean` for single indexes or an `array` of `boolean`s for multiple indexes. 148 | 149 | Usage is the same as the `visible` method. 150 | 151 | **As of `1.4.18` the `hidden()` method has been deprecated and will be removed in the next minor version.** 152 | 153 | --- 154 | 155 | ### `swap(indexes [array])` 156 | 157 | Swap th position of two columns. Just pass an array of 2 integers representing the column indexes you require swapping. 158 | 159 | ```javascript 160 | let columns = datatable.columns; 161 | 162 | // Swap the 1st and 6th columns 163 | columns.swap([0, 5]); 164 | ``` 165 | 166 | --- 167 | 168 | ### `order(indexes [array])` 169 | 170 | Order the columns based on the given order. Just pass an array of column indexes in the order you require: 171 | 172 | ### Original order 173 | 174 | ![Original order](http://i.imgur.com/OK5DoGs.png) 175 | 176 | ```javascript 177 | // Reorder the columns 178 | datatable.columns.order([1, 3, 4, 2, 0]); 179 | ``` 180 | 181 | ### Result 182 | 183 | ![Original order](http://i.imgur.com/kNGEgpT.png) 184 | -------------------------------------------------------------------------------- /docs/documentation/columns.md: -------------------------------------------------------------------------------- 1 | ### `columns` 2 | #### Type: `array` 3 | #### Default: `undefined` 4 | 5 | Controls various aspects of individual or groups of columns. Should be an array of objects with the following properties: 6 | 7 | #### `caseFirst` 8 | 9 | Default: `"false"` Options: `["false", "upper", "lower"]`. Influences how string sorting is done and whether upper or lower case letters are sorted first. If `"false"` is selected, will use the chosen `locale`'s default sorting order. Specifying `caseFirst` 10 | as part of the table's configuration will define a default for all columns. 11 | 12 | #### `cellClass` 13 | 14 | Allows to specify the CSS classes to apply to the body cell. 15 | 16 | #### `filter` 17 | 18 | Instead of ordering the row, clicking on the header will filter it. Specify an array of items to filter for. The array can also contain functions that will be executed on the data item of the cell to determine whether to include it in the filtered content. 19 | 20 | #### `format` 21 | 22 | A string representing the `datetime` format when using the `date` type. Specifying `format` 23 | as part of the table's configuration will define a default for all columns. 24 | 25 | #### `headerClass` 26 | 27 | Allows to specify the CSS classes to apply to the header cell. 28 | 29 | #### `hidden` 30 | 31 | When set to `true` the column(s) will not be visible and will be excluded from search results. 32 | 33 | #### `ignorePunctuation` 34 | 35 | Default: `true` (boolean). Influences how sorting and searching is done. Specifying `ignorePunctuation` as part of the table's configuration will define a default for all columns. 36 | 37 | #### `locale` 38 | 39 | Default: `"en-US"` (string). Set a locale such as `en-UK` or `de` for the column. Influences how string sorting is done. Allows even for specification of specific subvariants such as `de-DE-u-co-phonebk`. Specifying `locale` as part of the table's configuration will define a default for all columns. 40 | 41 | #### `numeric` 42 | 43 | Default: `true` (boolean). Influences how string sorting is done. If `true` multiple numbers will be seen as just one so that "file 1.jpg" will come before "file 100.jpg". Specifying `numeric` 44 | as part of the table's configuration will define a default for all columns. 45 | 46 | #### `searchable` 47 | 48 | When set to `false` the column(s) cannot be searched. 49 | 50 | #### `searchItemSeparator` 51 | 52 | Default: `""`. Influences searching in that cell content will be split with this value by default when searching for content. Specifying `searchItemSeparator` as part of the table's configuration will define a default for all search boxes. 53 | 54 | #### `searchMethod` 55 | 56 | A custom search method to be used for the column(s). The function should take 5 arguments: 57 | `terms` (an array of strings representing the search terms), 58 | `cell` (the cell that is to be checked for the search terms), 59 | `row` (the data row that the cell is part of), 60 | `column` (the id of the column of the cell), 61 | `source` (a unique string given to a particular search interface so that multiple search itnerfaces can be used simultaneously). 62 | 63 | It should return `true` if the search string is found in the data, `false` otherwise. 64 | 65 | The default is that it simply checks for the presence of a particular search term in the cell content. 66 | 67 | Defining a `searchMethod` as part of the table's configuration will define a default for all columns. 68 | 69 | #### `select` 70 | 71 | An integer or array of integers representing the column(s) to be manipulated. 72 | 73 | #### `sensitivity` 74 | 75 | Default: `"base"`. Options: `["base", "accent", "case", "variant"]`. Influences how searching is done. `"base"` and `"accent"` will ignore case differences. `"base"` and `"case"` will ignore differences in accent symbols. Specifying `sensitivity` as part of the table's configuration will define a default for all columns. 76 | 77 | #### `sort` 78 | 79 | Automatically sort the selected column. Can only be applied if a single column is selected. 80 | 81 | #### `sortSequence` 82 | 83 | An array of "asc" and "desc" describing the order sort will be executed as the user clicks on the column header. Note that each can only be mentioned up to one time. So effectively you have these options: 84 | 85 | ```js 86 | ["asc", "desc"] // default 87 | ["desc", "asc"] 88 | ["asc"] 89 | ["desc"] 90 | ``` 91 | 92 | #### `sortable` 93 | 94 | When set to `false` the column(s) cannot be sorted. 95 | 96 | #### `type` 97 | 98 | A `string` representing the type of data in the column(s) cells. Choose from the following options: 99 | 100 | * `html` (default) 101 | * `string` 102 | * `date` - a valid `datetime` string 103 | * `number` 104 | * `boolean` 105 | * `other` 106 | 107 | Specifying `type` as part of the table's configuration will define a default for all columns. 108 | 109 | #### `render` 110 | 111 | A callback to customise the rendering of the column(s) cell content. The function takes 4 parameters. 112 | You can either return a string representing the cells content, you can modify the provided td in the format used by [diffDOM](https://github.com/fiduswriter/diffDOM) or you can return a new td in that same format. 113 | 114 | ```javascript 115 | => 116 | render: function(value, td, rowIndex, cellIndex) { 117 | 118 | } 119 | 120 | ``` 121 | 122 | --- 123 | 124 | #### Examples 125 | ```javascript 126 | let datatable = new DataTable("#myTable", { 127 | columns: [ 128 | // Sort the second column in ascending order 129 | { select: 1, sort: "asc" }, 130 | 131 | // Set the third column as datetime string matching the format "DD/MM/YYY" 132 | { select: 2, type: "date", format: "DD/MM/YYYY" }, 133 | 134 | // Disable sorting on the fourth and fifth columns 135 | { select: [3,4], sortable: false }, 136 | 137 | // Hide the sixth column 138 | { select: 5, hidden: true }, 139 | 140 | // Append a button to the seventh column 141 | { 142 | select: 6, 143 | type: 'string', 144 | render: function(data, td, rowIndex, cellIndex) { 145 | return `${data}`; 146 | } 147 | } 148 | ] 149 | }); 150 | ``` 151 | 152 | You can use the same properties in your markup. Just add the property to the `th` element as a `data-{property}` attribute: 153 | 154 | ```html 155 | 156 | 157 | 158 | 159 | 160 | ... 161 | 162 | ... 163 |
Heading 1Heading 2Heading 3
164 | ``` 165 | -------------------------------------------------------------------------------- /docs/documentation/data.md: -------------------------------------------------------------------------------- 1 | ### `data` 2 | #### Type: `object` 3 | #### Default: `undefined` 4 | 5 | Pass an object of data to populate the table. 6 | 7 | You can set both the headings and rows with `headings` and `data` properties, respectively. The headings property is optional. 8 | 9 | ```javascript 10 | let myData = { 11 | "headings": [ 12 | "Name", 13 | "Company", 14 | "Ext.", 15 | "Start Date", 16 | "Email", 17 | "Phone No." 18 | ], 19 | "data": [ 20 | [ 21 | "Hedwig F. Nguyen", 22 | "Arcu Vel Foundation", 23 | "9875", 24 | "03/27/2017", 25 | "nunc.ullamcorper@metusvitae.com", 26 | "070 8206 9605" 27 | ], 28 | [ 29 | "Genevieve U. Watts", 30 | "Eget Incorporated", 31 | "9557", 32 | "07/18/2017", 33 | "Nullam.vitae@egestas.edu", 34 | "0800 106980" 35 | ], 36 | ... 37 | }; 38 | 39 | let dataTable = new DataTable(myTable, { 40 | data: myData 41 | }); 42 | ``` 43 | 44 | > NOTE: If the headings count and rows count do not match, the library will throw an exception. 45 | 46 | ### Using key-value pairs 47 | 48 | If your data is in the form of key-value pairs, you can quickly convert it to a format that the API can use: 49 | 50 | ```javascript 51 | 52 | let data = [ 53 | { 54 | "prop1": "value1", 55 | "prop2": "value2", 56 | "prop3": "value3" 57 | }, 58 | { 59 | "prop1": "value4", 60 | "prop2": "value5", 61 | "prop3": "value6" 62 | } 63 | ]; 64 | 65 | let obj = { 66 | // Quickly get the headings 67 | headings: Object.keys(data[0]), 68 | 69 | // data array 70 | data: [] 71 | }; 72 | 73 | // Loop over the objects to get the values 74 | for ( let i = 0; i < data.length; i++ ) { 75 | 76 | obj.data[i] = []; 77 | 78 | for (let p in data[i]) { 79 | if( data[i].hasOwnProperty(p) ) { 80 | obj.data[i].push(data[i][p]); 81 | } 82 | } 83 | } 84 | 85 | ``` 86 | 87 | which will produce: 88 | 89 | ```javascript 90 | { 91 | headings : [ 92 | "prop1", 93 | "prop2", 94 | "prop3" 95 | ], 96 | data : [ 97 | [ 98 | "value1", 99 | "value2", 100 | "value3" 101 | ], 102 | [ 103 | "value4", 104 | "value5", 105 | "value6" 106 | ] 107 | ] 108 | } 109 | ``` -------------------------------------------------------------------------------- /docs/documentation/datetime.md: -------------------------------------------------------------------------------- 1 | Simple DataTables utilizes the [MomentJS](https://momentjs.com/) library for parsing datetime strings for easier column sorting. 2 | 3 | ## Method 1 4 | 5 | Define a `data-type` attribute on the headings and set the value to `date`. If the datetime string is in a format that can not be sorted easily by standard methods, you must define the `data-format` attribute and set its value to the format that is expected. 6 | 7 | ```html 8 | 9 | 10 | 11 | 12 | ... 13 | 14 |
15 | ``` 16 | 17 | ## Method 2 18 | 19 | The `date` and `format` strings can also be defined in the options using the `columns` property: 20 | 21 | ```javascript 22 | // Allow sorting of the first column with "DD/MM/YYYY" format 23 | let datatable = new DataTable("#myTable", { 24 | columns: [ 25 | { 26 | select: 0, 27 | type: "date", 28 | format: "DD/MM/YYYY" 29 | } 30 | ] 31 | }); 32 | 33 | // Apply formatting to the third and fourth columns as well 34 | let datatable = new DataTable("#myTable", { 35 | columns: [ 36 | { 37 | select: 0, 38 | type: "date", 39 | format: "DD/MM/YYYY" 40 | }, 41 | { 42 | select: [2,3], 43 | type: "date", 44 | format: "MM/DD/YY" 45 | } 46 | ] 47 | }); 48 | ``` 49 | 50 | As well as custom format strings, there are some pre-defined formats that you can utilise: 51 | 52 | * `ISO_8601` 53 | * `RFC_2822` 54 | * `MYSQL` 55 | 56 | ```javascript 57 | // Allow sorting of the third column by MySQL datetime strings 58 | let datatable = new DataTable("#myTable", { 59 | columns: [ 60 | { 61 | select: 2, 62 | type: "date", 63 | format: "MYSQL" 64 | } 65 | ] 66 | }); 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/documentation/destroy().md: -------------------------------------------------------------------------------- 1 | #### type `Function` 2 | 3 | Destroy the instance. 4 | 5 | #### Usage 6 | ```javascript 7 | datatable.destroy(); 8 | ``` -------------------------------------------------------------------------------- /docs/documentation/destroyable.md: -------------------------------------------------------------------------------- 1 | ### `destroyable` 2 | #### Type: `boolean` 3 | 4 | ### Default 5 | ```javascript 6 | true 7 | ``` 8 | 9 | 10 | Whether to keep the original HTML in memory in order to be able to destroy the table again. 11 | -------------------------------------------------------------------------------- /docs/documentation/diffDomOptions.md: -------------------------------------------------------------------------------- 1 | ### `diffDomOptions` 2 | #### Type: `object` 3 | #### Default: `{ valueDiffing: false }` 4 | 5 | 6 | If specified, specifies options to apply to diffDOM. See diff-dom package for specifics. 7 | -------------------------------------------------------------------------------- /docs/documentation/ellipsisText.md: -------------------------------------------------------------------------------- 1 | ### `ellipsisText` 2 | #### Type: `string` 3 | #### Default: `'…'` 4 | 5 | Set the content of the ellipsis used in the pager. 6 | -------------------------------------------------------------------------------- /docs/documentation/firstLast.md: -------------------------------------------------------------------------------- 1 | ### `firstLast` 2 | #### Type: `boolean` 3 | #### Default: `false` 4 | 5 | Toggle the skip to first page and skip to last page buttons. -------------------------------------------------------------------------------- /docs/documentation/firstText.md: -------------------------------------------------------------------------------- 1 | ### `firstText` 2 | #### Type: `string` 3 | #### Default: `'«'` 4 | 5 | Set the content of the skip to first page button. 6 | -------------------------------------------------------------------------------- /docs/documentation/fixedColumns.md: -------------------------------------------------------------------------------- 1 | ### `fixedColumns` 2 | #### Type: `boolean` 3 | #### Default: `true` 4 | 5 | Fix the width of the columns. This stops the columns changing width when loading a new page. -------------------------------------------------------------------------------- /docs/documentation/fixedHeight.md: -------------------------------------------------------------------------------- 1 | ### 'fixedHeight` 2 | #### Type: `boolean` 3 | #### Default: `false` 4 | 5 | Fix the height of the table. This is useful if your last page contains less rows than set in the perPage options and 6 | simply stops the table from changing size and affecting the layout of the page. 7 | -------------------------------------------------------------------------------- /docs/documentation/footer.md: -------------------------------------------------------------------------------- 1 | ### `footer` 2 | #### Type: `boolean` 3 | #### Default: `false` 4 | 5 | Enable or disable the table footer. -------------------------------------------------------------------------------- /docs/documentation/header.md: -------------------------------------------------------------------------------- 1 | ### `header` 2 | #### Type: `boolean` 3 | #### Default: `true` 4 | 5 | Enable or disable the table header. -------------------------------------------------------------------------------- /docs/documentation/hiddenHeader.md: -------------------------------------------------------------------------------- 1 | ### `hiddenHeader` 2 | #### Type: `boolean` 3 | #### Default: `false` 4 | 5 | Whether to hide the table header. -------------------------------------------------------------------------------- /docs/documentation/index.md: -------------------------------------------------------------------------------- 1 | #### Getting Started 2 | * [Install](Getting-Started#install) 3 | * [Manual](Getting-Started#browser) 4 | * [Initialise](Getting-Started#initialise) 5 | * [Upgrading](Upgrading) 6 | 7 | #### Tips 8 | * [Sorting Datetimes](datetime) 9 | * [Dynamically Adding Data](Dynamically-adding-data) 10 | * [Add columns from remote source](Adding-a-column-from-a-remote-source) 11 | 12 | #### Events 13 | * [datatable.init](Events#datatableinit) 14 | * [datatable.refresh](Events#datatablerefresh) 15 | * [datatable.update](Events#datatableupdate) 16 | * [datatable.page](Events#datatablepage) 17 | * [datatable.sort](Events#datatablesort) 18 | * [datatable.perpage](Events#datatableperpage) 19 | * [datatable.search](Events#datatablesearch) 20 | * [datatable.multisearch](Events#datatablesearch) 21 | * [datatable.selectrow](Events#datatableselectrow) 22 | 23 | #### Options 24 | 25 | See [Options](Options) 26 | 27 | 28 | #### Properties 29 | * [dom](API#dom) 30 | * [wrapper](API#wrapper) 31 | * [container](API#container) 32 | * [pagers](API#pagers) 33 | * [options](API#options) 34 | * [initialized](API#initialized) 35 | * [data](API#data) 36 | * [data-index](API#data-index) 37 | * [pages](API#pages) 38 | * [hasRows](API#hasrows) 39 | * [hasHeadings](API#hasheadings) 40 | * [currentPage](API#currentpage) 41 | * [totalPages](API#totalpages) 42 | * [onFirstPage](API#onfirstpage) 43 | * [onLastPage](API#onlastpage) 44 | * [searching](API#searching) 45 | * [searchData](API#searchdata) 46 | 47 | #### Methods 48 | * [rows API](rows-API) 49 | * [columns API](columns-API) 50 | * [on()](on()) 51 | * [update()](update()) 52 | * [refresh()](refresh()) 53 | * [page()](page()) 54 | * [insert()](insert()) 55 | * [setMessage()](setMessage()) 56 | * [destroy()](destroy()) 57 | * [init()](init()) 58 | * [print()](print()) 59 | * [search()](search()) 60 | * [multiSearch()](multiSearch()) 61 | -------------------------------------------------------------------------------- /docs/documentation/init().md: -------------------------------------------------------------------------------- 1 | #### type `Function` 2 | 3 | Initialise the instance after [destroying](). 4 | 5 | #### Usage 6 | 7 | ```javascript 8 | /** 9 | * @return {Void} 10 | */ 11 | datatable.init(); 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/documentation/insert().md: -------------------------------------------------------------------------------- 1 | #### type `Function` 2 | 3 | Insert new data in to the table. 4 | #### Usage 5 | ```javascript 6 | /** 7 | * @param {object} data New data to insert 8 | * @return {void} 9 | */ 10 | datatable.insert(data); 11 | ``` 12 | 13 | --- 14 | 15 | #### Examples 16 | 17 | ##### Pass an `array` of `key-value` pairs (objects): 18 | ```javascript 19 | let newData = [ 20 | { 21 | "Heading 1": "Cell 1", 22 | "Heading 2": "Cell 2", 23 | "Heading 3": "Cell 3", 24 | "Heading 4": "Cell 4", 25 | }, 26 | { 27 | "Heading 1": "Cell 5", 28 | "Heading 2": "Cell 6", 29 | "Heading 3": "Cell 7", 30 | "Heading 4": "Cell 8", 31 | } 32 | ]; 33 | 34 | datatable.insert(newData); 35 | ``` 36 | 37 | ##### Pass an `object` with the `headings` and/or `data` property: 38 | 39 | ```javascript 40 | let newData = { 41 | headings: [ 42 | "Name", 43 | "Position", 44 | "Town", 45 | "Ext.", 46 | "Start Date", 47 | "Salary" 48 | ], 49 | data: [ 50 | [ 51 | "Cedric Kelly", 52 | "Senior Javascript Developer", 53 | "Edinburgh", 54 | "6224", 55 | "2012/03/29", 56 | "$433,060" 57 | ], 58 | [ 59 | "Airi Satou", 60 | "Accountant", 61 | "Tokyo", 62 | "5407", 63 | "2008/11/28", 64 | "$162,700" 65 | ], 66 | ... 67 | ] 68 | }; 69 | 70 | // add the rows 71 | dataTable.insert(newData); 72 | ``` 73 | 74 | If you attempt to pass new headings to a table that has headings, they'll be ignored. -------------------------------------------------------------------------------- /docs/documentation/labels.md: -------------------------------------------------------------------------------- 1 | ### `labels` 2 | #### Type: `object` 3 | 4 | Customise the displayed labels. (v1.0.6 and above) 5 | 6 | #### Defaults 7 | ```javascript 8 | labels: { 9 | placeholder: "Search...", 10 | searchTitle: "Search within table", 11 | pageTitle: "Page {page}", 12 | perPage: "entries per page", 13 | noRows: "No entries found", 14 | info: "Showing {start} to {end} of {rows} entries", 15 | noResults: "No results match your search query", 16 | } 17 | ``` 18 | 19 | The strings wrapped in curly braces represent variables that are inserted. 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 67 | 68 | 69 |
PropertyEffectVariables
placeholderSets the placeholder of the search inputNone
searchTitleSets the title of the search inputNone
pageTitleSets the title of the page (as used in the page navigator) 44 | {page} - The current page number
45 |
perPageSets the per-page dropdown's labelNone
noRowsThe message displayed when there are no search resultsNone
infoDisplays current range, page number, etc 61 | {start} - The first row number of the current page
62 | {end} - The last row number of the current page
63 | {page} - The current page number
64 | {pages} - Total pages
65 | {rows} - Total rows
66 |
70 | 71 | #### Example: 72 | 73 | ```javascript 74 | labels: { 75 | placeholder: "Search employees...", 76 | searchTitle: "Search within employees", 77 | perPage: "employees per page", 78 | noRows: "No employees to display", 79 | info: "Showing {start} to {end} of {rows} employees (Page {page} of {pages} pages)", 80 | }, 81 | ``` 82 | -------------------------------------------------------------------------------- /docs/documentation/lastText.md: -------------------------------------------------------------------------------- 1 | ### `lastText` 2 | #### Type: `string` 3 | #### Default: `'»'` 4 | 5 | Set the content of the skip to last page button. 6 | -------------------------------------------------------------------------------- /docs/documentation/multiSearch().md: -------------------------------------------------------------------------------- 1 | #### type `Function` 2 | 3 | Filter the table based on multiple queries. 4 | 5 | #### Usage 6 | 7 | ```javascript 8 | /** 9 | * @param { { terms: String[], columns=: Number[] }[] } queries Queries including search term and columns (optional) 10 | * @param {str} source Source of the search. (optional, used to invalidate search rows later on) 11 | * @return {void} 12 | */ 13 | datatable.multiSearch(queries); 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/documentation/nextPrev.md: -------------------------------------------------------------------------------- 1 | ### `nextPrev` 2 | #### Type: `integer` 3 | #### Default: `true` 4 | 5 | Toggle the next and previous pagination buttons. -------------------------------------------------------------------------------- /docs/documentation/nextText.md: -------------------------------------------------------------------------------- 1 | ### `nextText` 2 | #### Type: `string` 3 | #### Default: `'›'` 4 | 5 | Set the content on the next button. 6 | -------------------------------------------------------------------------------- /docs/documentation/on().md: -------------------------------------------------------------------------------- 1 | Listen for custom events. 2 | 3 | See the [Events API](Events). 4 | -------------------------------------------------------------------------------- /docs/documentation/page().md: -------------------------------------------------------------------------------- 1 | #### type `Function` 2 | 3 | Loads a given page. Page number must be an `integer`. 4 | 5 | #### Usage 6 | 7 | ```javascript 8 | /** 9 | * @param {int} num Page Number 10 | * @return {void} 11 | */ 12 | datatable.page(num); 13 | ``` -------------------------------------------------------------------------------- /docs/documentation/pagerDelta.md: -------------------------------------------------------------------------------- 1 | ### `pagerDelta` 2 | #### Type: `number` 3 | #### Default: `2` 4 | 5 | Delta used for truncating pager. 6 | -------------------------------------------------------------------------------- /docs/documentation/pagerRender.md: -------------------------------------------------------------------------------- 1 | ### `pagerRender` 2 | #### Type: `boolean` \ `function(data, ul)` 3 | #### Default: `false` 4 | 5 | 6 | If specified, declares a callback to customise the rendering of all pagers. The function can take 2 parameters: 7 | 8 | * **data**: an array of data relevant to render the pager: `[onFirstPage: boolean, onLastPage: boolean, currentPage: number, totalPages: number]`. 9 | 10 | * **ul**: the ul in the format used by (diffDOM)[https://github.com/fiduswriter/diffDOM]. 11 | 12 | You can either modify the ul in place, or you can return a new ul object in the same format. 13 | -------------------------------------------------------------------------------- /docs/documentation/paging.md: -------------------------------------------------------------------------------- 1 | ### `paging` 2 | #### Type: `boolean` 3 | #### Default: `true` 4 | 5 | Whether paging is enabled for the table. -------------------------------------------------------------------------------- /docs/documentation/perPage.md: -------------------------------------------------------------------------------- 1 | ### `perPage` 2 | #### Type: `integer` 3 | #### Default: `10` 4 | 5 | Sets the maximum number of rows to display on each page. -------------------------------------------------------------------------------- /docs/documentation/perPageSelect.md: -------------------------------------------------------------------------------- 1 | ### `perPageSelect` 2 | #### Type: `array` 3 | #### Default: `[5, 10, 15, 20, 25]` 4 | 5 | Sets the per page options in the dropdown. Must be an array of integers or arrays in the format [label (string), value (int)]. 6 | 7 | Values below 1 return all values. 8 | 9 | For example, you could specify the values in words like this: 10 | 11 | `[["Five", 5], ["Ten", 10], ["All", 0]]` 12 | 13 | Setting this to `false` will hide the dropdown. -------------------------------------------------------------------------------- /docs/documentation/prevText.md: -------------------------------------------------------------------------------- 1 | ### `prevText` 2 | #### Type: `string` 3 | #### Default: `'‹'` 4 | 5 | Set the content on the previous button. 6 | -------------------------------------------------------------------------------- /docs/documentation/print().md: -------------------------------------------------------------------------------- 1 | #### type `Function` 2 | 3 | Display printable version. 4 | 5 | #### Usage 6 | 7 | ```javascript 8 | datatable.print(); 9 | ``` -------------------------------------------------------------------------------- /docs/documentation/refresh().md: -------------------------------------------------------------------------------- 1 | #### type `Function` 2 | 3 | Refreshes the table. This will recount the rows, reset any search and remove any set message, but will not reset any sorting. 4 | 5 | #### Usage 6 | 7 | ```javascript 8 | datatable.refresh(); 9 | ``` -------------------------------------------------------------------------------- /docs/documentation/rowNavigation.md: -------------------------------------------------------------------------------- 1 | ### `rowNavigation` 2 | #### Type: `boolean` 3 | #### Default: `false` 4 | 5 | Enable or disable the navigation rows with keyboard. -------------------------------------------------------------------------------- /docs/documentation/rowRender.md: -------------------------------------------------------------------------------- 1 | ### `rowRender` 2 | #### Type: `boolean` \ `function(rowValue, tr, index)` 3 | #### Default: `false` 4 | 5 | 6 | If specified, declares a callback to customise the rendering of all rows. The function can take 3 parameters: 7 | 8 | * **rowValue**: the row as it exists in `dataTable.data.data`. 9 | 10 | * **tr**: the table row in the format used by (diffDOM)[https://github.com/fiduswriter/diffDOM]. 11 | 12 | * **index**: the integer representing the index of the row in `dataTable.data.data`. 13 | 14 | You can either modify the tr in place, or you can return a new tr object in the same format. 15 | -------------------------------------------------------------------------------- /docs/documentation/rowSelectionKeys.md: -------------------------------------------------------------------------------- 1 | ### `rowSelectionKeys` 2 | #### Type: `string[]` 3 | #### Default: `["Enter", " "]` 4 | 5 | Keys that will be monitored for row selection. If the key is pressed while a row is focused, a "datatable.selectrow" event 6 | will be triggered (see [Events](Events#datatableselectrow). 7 | 8 | The default keys are `Enter` and `Space`. 9 | -------------------------------------------------------------------------------- /docs/documentation/rows-API.md: -------------------------------------------------------------------------------- 1 | As of `v1.4.0`, the `rows` API is implemented and allows access to the table rows for quick manipulation. 2 | 3 | As of `v4.0.0`, `rows` is a property and not a method on the current instance. 4 | 5 | To use the `rows` API just access under the `rows` property of the current instance: 6 | 7 | ```javascript 8 | let rows = datatable.rows; 9 | ``` 10 | 11 | You can then chain the following methods. 12 | 13 | --- 14 | 15 | ### `add(data [array])` 16 | 17 | Add new row data to the current instance. The `data` parameter must be an `array` of `strings` to be inserted into each of the new row's cells. 18 | 19 | ```javascript 20 | let newRow = ["column1", "column2", "column3", "column4", ...]; 21 | 22 | dataTable.rows.add(newRow); 23 | 24 | ``` 25 | 26 | **Note:** Only one row can be added at a time. If you want to add multiple rows simultaneously, do this instead: 27 | 28 | ```javascript 29 | let newRows = [ 30 | ["column1", "column2", "column3", "column4", ...], 31 | ["column1", "column2", "column3", "column4", ...], 32 | ["column1", "column2", "column3", "column4", ...], 33 | ["column1", "column2", "column3", "column4", ...], 34 | ... 35 | ]; 36 | 37 | dataTable.insert({data: newRows}) 38 | ``` 39 | 40 | --- 41 | 42 | ### `remove(select [array|number])` 43 | 44 | Remove existing rows from the current instance. The `select` parameter can either be an `integer` or `array` of `integers` representing the row indexes. 45 | 46 | ```javascript 47 | let rows = dataTable.rows; 48 | 49 | // remove the 6th row 50 | rows.remove(5); 51 | 52 | // remove the first 5 rows 53 | rows.remove([0,1,2,3,4]); 54 | 55 | ``` 56 | 57 | Note that the indexes passed to this method should represent the actual index of the row in the [`data`](API#data) array. The native [`rowIndex`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableRowElement/rowIndex) property represents the position of a row in the rendered page and may be different to the index you need to pass to the `remove()` method. 58 | 59 | For example, if you're trying to remove a row that's unrendered, the `rowIndex` property will return `-1`. 60 | 61 | Another example would be if you're currently on page 5 and you have `perPage` set to `5` the currently rendered rows have a `rowIndex` of `0`, `1`, `2`, `3` and `4`, respectively, but to remove them you would need to use the indexes `20`, `21`, `22`, `23` and `24`, respectively. 62 | 63 | ```javascript 64 | let rows = dataTable.rows; 65 | 66 | // Switch to page 5 67 | dataTable.page(5); 68 | 69 | // WRONG: removes the first 5 rows on page 1 70 | rows.remove([0, 1, 2, 3, 4]); 71 | 72 | // CORRECT: removes the 5 currently rendered rows on page 5 73 | rows.remove([20, 21, 22, 23, 24]); 74 | ``` 75 | 76 | You can quickly access the correct index for the rendered row by grabbing it's `dataset-index` property as opposed to the `rowIndex` property. 77 | 78 | ```javascript 79 | // Get the first rendered row 80 | let rowToRemove = dataTable.body.querySelector("tr"); 81 | 82 | // Remove it 83 | dataTable.rows.remove(parseInt(rowToRemove.dataset.index)); 84 | 85 | ``` 86 | 87 | --- 88 | -------------------------------------------------------------------------------- /docs/documentation/scrollY.md: -------------------------------------------------------------------------------- 1 | ### `scrollY` 2 | #### Type: `string` 3 | #### Default: `""` 4 | 5 | Enable vertical scrolling. Vertical scrolling will constrain the DataTable to the given height, and enable scrolling for any data which overflows the current viewport. This can be used as an alternative to paging to display a lot of data in a small area. 6 | 7 | The value given here can be given in any CSS unit. -------------------------------------------------------------------------------- /docs/documentation/search().md: -------------------------------------------------------------------------------- 1 | #### type `Function` 2 | 3 | Filter the table based on a single query. 4 | 5 | #### Usage 6 | 7 | ```javascript 8 | /** 9 | * @param {str} term Search term 10 | * @param {int[]} columns Columns to be searched (optional) 11 | * @param {str} source Source of the search. (optional, used to invalidate search rows later on) 12 | * @return {void} 13 | */ 14 | datatable.search(term, columns); 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/documentation/searchMethod.md: -------------------------------------------------------------------------------- 1 | ### `searchMethod` 2 | #### Type: `function` 3 | #### Default: `undefined` 4 | 5 | A custom search method to be used for the column(s). The function should take 5 arguments: 6 | `terms` (an array of strings representing the search terms), 7 | `cell` (the cell that is to be checked for the search terms), 8 | `row` (the data row that the cell is part of), 9 | `column` (the id of the column of the cell), 10 | `source` (a unique string given to a particular search interface so that multiple search itnerfaces can be used simultaneously). 11 | 12 | It should return `true` if the search string is found in the data, `false` otherwise. 13 | 14 | The default is that it simply checks for the presence of a particular search term in the cell content. 15 | 16 | A `searchMethod` can also be defined for individual columns. If a `searchMethod` is defined for a column, it will override the 17 | default `searchMethod` defined for the table. 18 | -------------------------------------------------------------------------------- /docs/documentation/searchQuerySeparator.md: -------------------------------------------------------------------------------- 1 | ### `searchQuerySeparator` 2 | #### Type: `string` 3 | #### Default: `" "` 4 | 5 | Influences searching in that search queries will be split with this value by default when searchign through a search box. 6 | -------------------------------------------------------------------------------- /docs/documentation/searchable.md: -------------------------------------------------------------------------------- 1 | ### `searchable` 2 | #### Type: `boolean` 3 | #### Default: `true` 4 | 5 | Toggle the ability to search the dataset -------------------------------------------------------------------------------- /docs/documentation/setMessage().md: -------------------------------------------------------------------------------- 1 | #### type `Function` 2 | 3 | Display a message in the table. 4 | 5 | #### Usages 6 | ```javascript 7 | /** 8 | * @param {string} message The message to show 9 | * @return {void} 10 | */ 11 | dataTable.setMessage(message); 12 | ``` -------------------------------------------------------------------------------- /docs/documentation/sortable.md: -------------------------------------------------------------------------------- 1 | ### `sortable` 2 | #### Type: `boolean` 3 | #### Default: `true` 4 | 5 | Toggle the ability to sort the columns. 6 | 7 | > This option will be forced to `false` if the table has no headings. -------------------------------------------------------------------------------- /docs/documentation/tabIndex.md: -------------------------------------------------------------------------------- 1 | ### `tabIndex` 2 | #### Type: `boolean` 3 | #### Default: `false` 4 | 5 | If specified, declares the tab index number to be used for the table. This is useful in combination with [rowNavigation](rowNavigation). 6 | -------------------------------------------------------------------------------- /docs/documentation/tableRender.md: -------------------------------------------------------------------------------- 1 | ### `tableRender` 2 | #### Type: `boolean` \ `function(data, table, type)` 3 | #### Default: `false` 4 | 5 | 6 | If specified, declares a callback to customise the rendering of all tables. The function can take 3 parameters: 7 | 8 | * **data**: the current state of the data as it exists in `dataTable.data`. 9 | 10 | * **table**: the table in the format used by (diffDOM)[https://github.com/fiduswriter/diffDOM]. 11 | 12 | * **type**: a string representing the kind of table that is being drawn: `"main"` is the regular table, `"message"` is an empty table that is to include a message to the user, `"header"` is a special header table used for `scrollY`-tables, and `"print"` when the table is being rendered for printing. 13 | 14 | You can either modify the table in place, or you can return a new table object in the same format. 15 | -------------------------------------------------------------------------------- /docs/documentation/template.md: -------------------------------------------------------------------------------- 1 | ### `template` 2 | #### Type: `function` 3 | 4 | ### Default 5 | 6 | ```javascript 7 | (options, dom) => `
8 | ${ 9 | options.paging && options.perPageSelect ? 10 | `
11 | 14 |
` : 15 | "" 16 | } 17 | ${ 18 | options.searchable ? 19 | `
20 | 21 |
` : 22 | "" 23 | } 24 |
25 |
26 |
27 | ${ 28 | options.paging ? 29 | `
` : 30 | "" 31 | } 32 | 33 |
` 34 | ``` 35 | 36 | Allows for custom arranging of the DOM elements in the top and bottom containers. Be aware that several of the class names are used to position specific parts of the table, such as the pager (`options.classes.pagination`), the table itself (`options.classes.container`) and the pagination selector (`options.classes.selector`). There can be several pagers. 37 | 38 | There can be multiple search fields and you can influence how they operate by adding `data-column`, `data-query-separator` and `data-and` attributes to them. `data-column` is to be a JSON array of column indices that the search is to operate on. The `data-query-separator` will be used to split the search value for individual search items. The `data-and` attribute will change the search from an OR-search to an AND-search. Default values can be specified as `searchQuerySeparator` and `searchAnd` options. 39 | 40 | For example: `` 41 | -------------------------------------------------------------------------------- /docs/documentation/truncatePager.md: -------------------------------------------------------------------------------- 1 | ### `truncatePager` 2 | #### Type: `boolean` 3 | #### Default: `true` 4 | 5 | Truncate the page links to prevent overflow with large datasets. -------------------------------------------------------------------------------- /docs/documentation/update().md: -------------------------------------------------------------------------------- 1 | #### type `Function` 2 | 3 | Updates the dom of the table. Needs to be called after you change the data of `datatable.data` manually or you changed things inside of `datatable.columns.settings`, etc. The argument `measureWidths` will determine whether the needed widths of the columns will be measured again. The columns need to measured again if any change that was made could have lead to differently sized columns. 4 | 5 | #### Usage 6 | 7 | ```javascript 8 | datatable.update(measureWidths=false); 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiduswriter/simple-datatables/e070774d1e07647ea335d53bba20cd520b76826a/docs/favicon.ico -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiduswriter/simple-datatables/e070774d1e07647ea335d53bba20cd520b76826a/docs/favicon.png -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | simple-datatables 7 | 8 | 9 | 10 | 11 |

simple-datatables

12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-datatables", 3 | "version": "10.0.0", 4 | "description": "A lightweight, dependency-free JavaScript HTML table plugin.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "jsdelivr": "dist/umd/simple-datatables.js", 8 | "unpkg": "dist/umd/simple-datatables.js", 9 | "module": "dist/module.js", 10 | "style": "dist/style.css", 11 | "scripts": { 12 | "test": "mocha", 13 | "test_server": "node test/server.mjs", 14 | "lint": "eslint src/ docs/demos/ test/", 15 | "pre-commit": "eslint --fix src/ docs/demos/ test/", 16 | "build": "npm run build_js && npm run build_js_umd && npm run build_css && npm run build_demos", 17 | "build_js": "rollup -c", 18 | "build_js_umd": "browserify dist/index.js --standalone simpleDatatables -o dist/umd/simple-datatables.js", 19 | "build_css": "shx cp -R src/css/* dist/", 20 | "build_demos": "npm run build_demos_js && shx cp -R src/css docs/demos/dist/", 21 | "build_demos_js": "rollup -c rollup.demos.config.mjs", 22 | "postbuild_demos": "shx cp -r dist/umd/simple-datatables.js docs/demos/dist/umd.js", 23 | "prepare": "npm run build" 24 | }, 25 | "pre-commit": [ 26 | "pre-commit" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/fiduswriter/simple-datatables.git" 31 | }, 32 | "keywords": [ 33 | "DataTable", 34 | "DataTables", 35 | "table", 36 | "html table", 37 | "filter", 38 | "sort" 39 | ], 40 | "author": "Johannes Wilm", 41 | "license": "LGPL-3.0", 42 | "bugs": { 43 | "url": "https://github.com/fiduswriter/simple-datatables/issues" 44 | }, 45 | "homepage": "https://github.com/fiduswriter/simple-datatables#readme", 46 | "devDependencies": { 47 | "@babel/core": "^7.24.4", 48 | "@html-eslint/eslint-plugin": "^0.24.1", 49 | "@html-eslint/parser": "^0.24.1", 50 | "@rollup/plugin-commonjs": "^25.0.7", 51 | "@rollup/plugin-node-resolve": "^15.2.3", 52 | "@rollup/plugin-terser": "^0.4.4", 53 | "@rollup/plugin-typescript": "^11.1.6", 54 | "@types/node": "^20.12.7", 55 | "@typescript-eslint/eslint-plugin": "^7.6.0", 56 | "@typescript-eslint/parser": "^7.6.0", 57 | "browserify": "^17.0.0", 58 | "chromedriver": "*", 59 | "eslint": "^8.56.0", 60 | "eslint-plugin-htm": "^0.6.0", 61 | "eslint-plugin-html": "^8.1.0", 62 | "express": "^4.19.2", 63 | "get-port": "^7.1.0", 64 | "mocha": "^10.4.0", 65 | "mocha-each": "^2.0.1", 66 | "pre-commit": "^1.2.2", 67 | "rollup": "^4.14.1", 68 | "rollup-plugin-dts": "^6.1.0", 69 | "selenium-webdriver": "^4.19.0", 70 | "shx": "^0.3.4", 71 | "tslib": "^2.6.2", 72 | "typescript": "^5.4.5" 73 | }, 74 | "dependencies": { 75 | "dayjs": "^1.11.10", 76 | "diff-dom": "^5.1.3" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs' 2 | import dts from 'rollup-plugin-dts' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | import terser from '@rollup/plugin-terser' 5 | import typescript from "@rollup/plugin-typescript" 6 | 7 | export default [ 8 | { 9 | input: 'src/index.ts', 10 | plugins: [ 11 | resolve({browser: true}), 12 | typescript(), 13 | commonjs(), 14 | terser() 15 | ], 16 | output: // ES module version, for modern browsers 17 | [ 18 | { 19 | file: "dist/index.js", 20 | format: "cjs", 21 | sourcemap: true, 22 | }, 23 | { 24 | file: "dist/module.js", 25 | format: "es", 26 | sourcemap: true, 27 | }, 28 | { 29 | file: "dist/nomodule.js", 30 | format: "system", 31 | sourcemap: true, 32 | }, 33 | ], 34 | }, 35 | { 36 | // path to your declaration files root 37 | input: './dist/dts/index.d.ts', 38 | output: [{ file: 'dist/index.d.ts', format: 'es' }], 39 | plugins: [dts()], 40 | }, 41 | ] 42 | -------------------------------------------------------------------------------- /rollup.demos.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs' 2 | import resolve from '@rollup/plugin-node-resolve' 3 | import typescript from "@rollup/plugin-typescript" 4 | 5 | export default [ 6 | { 7 | input: 'src/index.ts', 8 | plugins: [ 9 | resolve({browser: true}), 10 | typescript({ 11 | compilerOptions: { 12 | declaration: false, 13 | declarationDir: undefined 14 | } 15 | }), 16 | commonjs() 17 | ], 18 | output: { 19 | file: "docs/demos/dist/module.js", 20 | inlineDynamicImports: true, 21 | format: "es", 22 | sourcemap: true 23 | } 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /src/column_filter/column_filter.ts: -------------------------------------------------------------------------------- 1 | import {DataTable} from "../datatable" 2 | import {classNamesToSelector, createElement} from "../helpers" 3 | 4 | import { 5 | defaultConfig 6 | } from "./config" 7 | 8 | import {ColumnFilterOptions} from "./types" 9 | 10 | class ColumnFilter { 11 | 12 | addedButtonDOM: boolean 13 | 14 | menuOpen: boolean 15 | 16 | buttonDOM: HTMLElement 17 | 18 | dt: DataTable 19 | 20 | events: { [key: string]: () => void} 21 | 22 | initialized: boolean 23 | 24 | options: ColumnFilterOptions 25 | 26 | menuDOM: HTMLElement 27 | 28 | containerDOM: HTMLElement 29 | 30 | wrapperDOM: HTMLElement 31 | 32 | limits: {x: number, y: number} 33 | 34 | rect: {width: number, height: number} 35 | 36 | event: Event 37 | 38 | constructor(dataTable: DataTable, options = {}) { 39 | this.dt = dataTable 40 | this.options = { 41 | ...defaultConfig, 42 | ...options 43 | } 44 | } 45 | 46 | init() { 47 | 48 | if (this.initialized) { 49 | return 50 | } 51 | 52 | const buttonSelector = classNamesToSelector(this.options.classes.button) 53 | let buttonDOM : (HTMLElement | null) = this.dt.wrapperDOM.querySelector(buttonSelector) 54 | if (!buttonDOM) { 55 | buttonDOM = createElement( 56 | "button", 57 | { 58 | class: this.options.classes.button, 59 | html: "▦" 60 | } 61 | ) 62 | // filter button not part of template (could be default template. We add it to search.) 63 | const searchSelector = classNamesToSelector(this.dt.options.classes.search) 64 | const searchWrapper = this.dt.wrapperDOM.querySelector(searchSelector) 65 | if (searchWrapper) { 66 | searchWrapper.appendChild(buttonDOM) 67 | } else { 68 | this.dt.wrapperDOM.appendChild(buttonDOM) 69 | } 70 | this.addedButtonDOM = true 71 | } 72 | this.buttonDOM = buttonDOM 73 | 74 | 75 | this.containerDOM = createElement("div", { 76 | id: this.options.classes.container 77 | }) 78 | this.wrapperDOM = createElement("div", { 79 | class: this.options.classes.wrapper 80 | }) 81 | this.menuDOM = createElement("ul", { 82 | class: this.options.classes.menu, 83 | html: this.dt.data.headings.map( 84 | (heading, index) => { 85 | const settings = this.dt.columns.settings[index] 86 | if (this.options.hiddenColumns.includes(index)) { 87 | return "" 88 | } 89 | return `
  • 90 | 91 | 94 |
  • ` 95 | } 96 | ).join("") 97 | }) 98 | this.wrapperDOM.appendChild(this.menuDOM) 99 | this.containerDOM.appendChild(this.wrapperDOM) 100 | this._measureSpace() 101 | 102 | this._bind() 103 | 104 | this.initialized = true 105 | 106 | } 107 | 108 | dismiss() { 109 | if (this.addedButtonDOM && this.buttonDOM.parentElement) { 110 | this.buttonDOM.parentElement.removeChild(this.buttonDOM) 111 | } 112 | document.removeEventListener("click", this.events.click) 113 | } 114 | 115 | _bind() { 116 | this.events = { 117 | click: this._click.bind(this) 118 | } 119 | document.addEventListener("click", this.events.click) 120 | } 121 | 122 | _openMenu() { 123 | document.body.appendChild(this.containerDOM) 124 | this._measureSpace() 125 | this.menuOpen = true 126 | this.dt.emit("columnFilter.menu.open") 127 | } 128 | 129 | _closeMenu() { 130 | if (this.menuOpen) { 131 | this.menuOpen = false 132 | document.body.removeChild(this.containerDOM) 133 | this.dt.emit("columnFilter.menu.close") 134 | } 135 | } 136 | 137 | _measureSpace() { 138 | const scrollX = window.scrollX || window.pageXOffset 139 | const scrollY = window.scrollY || window.pageYOffset 140 | this.rect = this.wrapperDOM.getBoundingClientRect() 141 | this.limits = { 142 | x: window.innerWidth + scrollX - this.rect.width, 143 | y: window.innerHeight + scrollY - this.rect.height 144 | } 145 | } 146 | 147 | _click(event: MouseEvent) { 148 | const target = event.target 149 | if (!(target instanceof Element)) { 150 | return 151 | } 152 | this.event = event 153 | 154 | if (this.buttonDOM.contains(target)) { 155 | event.preventDefault() 156 | if (this.menuOpen) { 157 | this._closeMenu() 158 | return 159 | } 160 | this._openMenu() 161 | // get the mouse position 162 | let x = event.pageX 163 | let y = event.pageY 164 | // check if we're near the right edge of window 165 | if (x > this.limits.x) { 166 | x -= this.rect.width 167 | } 168 | // check if we're near the bottom edge of window 169 | if (y > this.limits.y) { 170 | y -= this.rect.height 171 | } 172 | this.wrapperDOM.style.top = `${y}px` 173 | this.wrapperDOM.style.left = `${x}px` 174 | } else if (this.menuDOM.contains(target)) { 175 | const menuSelector = classNamesToSelector(this.options.classes.menu) 176 | const li = target.closest(`${menuSelector} > li`) as HTMLElement 177 | if (!li) { 178 | return 179 | } 180 | const checkbox = li.querySelector("input[type=checkbox]") as HTMLInputElement 181 | if (!checkbox.contains(target)) { 182 | checkbox.checked = !checkbox.checked 183 | } 184 | const column = Number(li.dataset.column) 185 | if (checkbox.checked) { 186 | this.dt.columns.show([column]) 187 | } else { 188 | this.dt.columns.hide([column]) 189 | } 190 | } else if (this.menuOpen) { 191 | this._closeMenu() 192 | } 193 | } 194 | 195 | } 196 | 197 | 198 | export const addColumnFilter = function(dataTable: DataTable, options = {}) { 199 | const columnFilter = new ColumnFilter(dataTable, options) 200 | if (dataTable.initialized) { 201 | columnFilter.init() 202 | } else { 203 | dataTable.on("datatable.init", () => columnFilter.init()) 204 | } 205 | 206 | return columnFilter 207 | } 208 | -------------------------------------------------------------------------------- /src/column_filter/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default config 3 | * @type {Object} 4 | */ 5 | //import {ColumnFilter} from "./column_filter" 6 | 7 | export const defaultConfig = { 8 | classes: { 9 | button: "datatable-column-filter-button", 10 | menu: "datatable-column-filter-menu", 11 | container: "datatable-column-filter-container", 12 | wrapper: "datatable-column-filter-wrapper" 13 | }, 14 | labels: { 15 | button: "Filter columns within the table" // The filter button title 16 | }, 17 | hiddenColumns: [] 18 | } 19 | -------------------------------------------------------------------------------- /src/column_filter/index.ts: -------------------------------------------------------------------------------- 1 | export {addColumnFilter} from "./column_filter" 2 | -------------------------------------------------------------------------------- /src/column_filter/types.ts: -------------------------------------------------------------------------------- 1 | interface ColumnFilterOptions { 2 | classes?: { 3 | button?: string, 4 | menu?: string, 5 | wrapper?: string, 6 | container?: string 7 | }, 8 | labels?: { 9 | button?: string 10 | }, 11 | hiddenColumns?: number[]; 12 | } 13 | 14 | export { 15 | ColumnFilterOptions 16 | } 17 | -------------------------------------------------------------------------------- /src/column_settings.ts: -------------------------------------------------------------------------------- 1 | import { 2 | columnsStateType, 3 | filterStateType, 4 | columnSettingsType 5 | } from "./types" 6 | 7 | export const readColumnSettings = (columnOptions = [], defaultType, defaultFormat) : [columnSettingsType[], columnsStateType] => { 8 | 9 | let columns: (columnSettingsType | undefined)[] = [] 10 | let sort: (false | {column: number, dir: "asc" | "desc"}) = false 11 | const filters: (filterStateType | undefined )[] = [] 12 | 13 | // Check for the columns option 14 | 15 | columnOptions.forEach(data => { 16 | 17 | // convert single column selection to array 18 | const columnSelectors = Array.isArray(data.select) ? data.select : [data.select] 19 | 20 | columnSelectors.forEach((selector: number) => { 21 | if (columns[selector]) { 22 | if (data.type) { 23 | columns[selector].type = data.type 24 | } 25 | } else { 26 | columns[selector] = { 27 | type: data.type || defaultType, 28 | sortable: true, 29 | searchable: true 30 | } 31 | } 32 | const column = columns[selector] 33 | 34 | 35 | if (data.render) { 36 | column.render = data.render 37 | } 38 | 39 | if (data.format) { 40 | column.format = data.format 41 | } else if (data.type === "date") { 42 | column.format = defaultFormat 43 | } 44 | 45 | if (data.cellClass) { 46 | column.cellClass = data.cellClass 47 | } 48 | 49 | if (data.headerClass) { 50 | column.headerClass = data.headerClass 51 | } 52 | 53 | if (data.locale) { 54 | column.locale = data.locale 55 | } 56 | 57 | if (data.sortable === false) { 58 | column.sortable = false 59 | } else { 60 | if (data.numeric) { 61 | column.numeric = data.numeric 62 | } 63 | if (data.caseFirst) { 64 | column.caseFirst = data.caseFirst 65 | } 66 | } 67 | 68 | if (data.searchable === false) { 69 | column.searchable = false 70 | } else { 71 | if (data.sensitivity) { 72 | column.sensitivity = data.sensitivity 73 | } 74 | } 75 | 76 | if (column.searchable || column.sortable) { 77 | if (typeof data.ignorePunctuation !== "undefined") { 78 | column.ignorePunctuation = data.ignorePunctuation 79 | } 80 | } 81 | 82 | if (data.searchMethod) { 83 | column.searchMethod = data.searchMethod 84 | } 85 | 86 | if (data.hidden) { 87 | column.hidden = true 88 | } 89 | 90 | if (data.filter) { 91 | column.filter = data.filter 92 | } 93 | 94 | if (data.sortSequence) { 95 | column.sortSequence = data.sortSequence 96 | } 97 | 98 | if (data.sort) { 99 | if (data.filter) { 100 | filters[selector] = data.sort 101 | } else { 102 | // We only allow one. The last one will overwrite all other options 103 | sort = {column: selector, 104 | dir: data.sort} 105 | } 106 | } 107 | 108 | if (typeof data.searchItemSeparator !== "undefined") { 109 | column.searchItemSeparator = data.searchItemSeparator 110 | } 111 | 112 | }) 113 | 114 | 115 | }) 116 | 117 | columns = columns.map(column => column ? 118 | column : 119 | {type: defaultType, 120 | format: defaultType === "date" ? defaultFormat : undefined, 121 | sortable: true, 122 | searchable: true}) 123 | 124 | const widths = [] // Width are determined later on by measuring on screen. 125 | 126 | return [ 127 | columns, {filters, 128 | sort, 129 | widths} 130 | ] 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import {DataTableConfiguration} from "./types" 2 | import {layoutTemplate} from "./templates" 3 | /** 4 | * Default configuration 5 | */ 6 | export const defaultConfig: DataTableConfiguration = { 7 | // for sorting 8 | sortable: true, 9 | locale: "en", 10 | numeric: true, 11 | caseFirst: "false", 12 | 13 | // for searching 14 | searchable: true, 15 | sensitivity: "base", 16 | ignorePunctuation: true, 17 | destroyable: true, 18 | searchItemSeparator: "", // If specified, splits the content of cells up using this separator before performing search. 19 | searchQuerySeparator: " ", 20 | searchAnd: false, 21 | searchMethod: false, // Custom search method to use. If not false, this will override built-in search methods. 22 | 23 | // data 24 | data: {}, 25 | type: "html", // Default data type for columns. 26 | format: "YYYY-MM-DD", 27 | columns: [], 28 | 29 | // Pagination 30 | paging: true, 31 | perPage: 10, 32 | perPageSelect: [5, 10, 15, 20, 25], 33 | nextPrev: true, 34 | firstLast: false, 35 | prevText: "‹", 36 | nextText: "›", 37 | firstText: "«", 38 | lastText: "»", 39 | ellipsisText: "…", 40 | truncatePager: true, 41 | pagerDelta: 2, 42 | 43 | scrollY: "", 44 | 45 | fixedColumns: true, 46 | fixedHeight: false, 47 | 48 | footer: false, 49 | header: true, 50 | hiddenHeader: false, 51 | caption: undefined, 52 | 53 | // for keyboard navigation 54 | rowNavigation: false, 55 | rowSelectionKeys: ["Enter", " "], 56 | tabIndex: false, 57 | 58 | // for overriding rendering 59 | pagerRender: false, 60 | rowRender: false, 61 | tableRender: false, 62 | diffDomOptions: { 63 | valueDiffing: false 64 | }, 65 | 66 | // Customise the display text 67 | labels: { 68 | placeholder: "Search...", // The search input placeholder 69 | searchTitle: "Search within table", // The search input title 70 | perPage: "entries per page", // per-page dropdown label 71 | pageTitle: "Page {page}", // page label used in Aria-label 72 | noRows: "No entries found", // Message shown when there are no records to show 73 | noResults: "No results match your search query", // Message shown when there are no search results 74 | info: "Showing {start} to {end} of {rows} entries" // 75 | }, 76 | 77 | // Customise the layout 78 | template: layoutTemplate, 79 | 80 | // Customize the class names used by datatable for different parts 81 | classes: { 82 | active: "datatable-active", 83 | ascending: "datatable-ascending", 84 | bottom: "datatable-bottom", 85 | container: "datatable-container", 86 | cursor: "datatable-cursor", 87 | descending: "datatable-descending", 88 | disabled: "datatable-disabled", 89 | dropdown: "datatable-dropdown", 90 | ellipsis: "datatable-ellipsis", 91 | filter: "datatable-filter", 92 | filterActive: "datatable-filter-active", 93 | empty: "datatable-empty", 94 | headercontainer: "datatable-headercontainer", 95 | hidden: "datatable-hidden", 96 | info: "datatable-info", 97 | input: "datatable-input", 98 | loading: "datatable-loading", 99 | pagination: "datatable-pagination", 100 | paginationList: "datatable-pagination-list", 101 | paginationListItem: "datatable-pagination-list-item", 102 | paginationListItemLink: "datatable-pagination-list-item-link", 103 | search: "datatable-search", 104 | selector: "datatable-selector", 105 | sorter: "datatable-sorter", 106 | table: "datatable-table", 107 | top: "datatable-top", 108 | wrapper: "datatable-wrapper" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/convert/csv.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isObject 3 | } from "../helpers" 4 | 5 | interface csvConvertUserOptions { 6 | lineDelimiter?: string, 7 | columnDelimiter?: string 8 | removeDoubleQuotes?: boolean 9 | data: string, 10 | headings?: string[], 11 | } 12 | 13 | 14 | /** 15 | * Convert CSV data to fit the format used in the table. 16 | */ 17 | export const convertCSV = function(userOptions : csvConvertUserOptions) { 18 | let obj 19 | const defaults = { 20 | lineDelimiter: "\n", 21 | columnDelimiter: ",", 22 | removeDoubleQuotes: false 23 | } 24 | 25 | // Check for the options object 26 | if (!isObject(userOptions)) { 27 | return false 28 | } 29 | 30 | const options = { 31 | ...defaults, 32 | ...userOptions 33 | } 34 | 35 | if (options.data.length) { 36 | // Import CSV 37 | obj = { 38 | data: [] 39 | } 40 | 41 | // Split the string into rows 42 | const rows : string[] = options.data.split(options.lineDelimiter) 43 | 44 | if (rows.length) { 45 | 46 | if (options.headings) { 47 | obj.headings = rows[0].split(options.columnDelimiter) 48 | if (options.removeDoubleQuotes) { 49 | obj.headings = obj.headings.map((e: string) => e.trim().replace(/(^"|"$)/g, "")) 50 | } 51 | rows.shift() 52 | } 53 | 54 | rows.forEach((row: string, i: number) => { 55 | obj.data[i] = [] 56 | 57 | // Split the rows into values 58 | const values = row.split(options.columnDelimiter) 59 | 60 | if (values.length) { 61 | values.forEach((value: string) => { 62 | if (options.removeDoubleQuotes) { 63 | value = value.trim().replace(/(^"|"$)/g, "") 64 | } 65 | obj.data[i].push(value) 66 | }) 67 | } 68 | }) 69 | } 70 | 71 | if (obj) { 72 | return obj 73 | } 74 | } 75 | 76 | return false 77 | } 78 | -------------------------------------------------------------------------------- /src/convert/index.ts: -------------------------------------------------------------------------------- 1 | export {convertCSV} from "./csv" 2 | export {convertJSON} from "./json" 3 | -------------------------------------------------------------------------------- /src/convert/json.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isJson, 3 | isObject 4 | } from "../helpers" 5 | 6 | interface jsonConvertUserOptions { 7 | lineDelimiter?: string, 8 | columnDelimiter?: string 9 | removeDoubleQuotes?: boolean 10 | data: string, 11 | headings?: string[], 12 | } 13 | 14 | /** 15 | * Convert JSON data to fit the format used in the table. 16 | */ 17 | export const convertJSON = function(userOptions : jsonConvertUserOptions) { 18 | let obj 19 | const defaults = { 20 | data: "" 21 | } 22 | 23 | // Check for the options object 24 | if (!isObject(userOptions)) { 25 | return false 26 | } 27 | 28 | const options = { 29 | ...defaults, 30 | ...userOptions 31 | } 32 | 33 | if (options.data.length || isObject(options.data)) { 34 | // Import JSON 35 | const json = isJson(options.data) ? JSON.parse(options.data) : false 36 | 37 | // Valid JSON string 38 | if (json) { 39 | obj = { 40 | headings: [], 41 | data: [] 42 | } 43 | 44 | json.forEach((data: { [key: string]: string | number | boolean | null | undefined}, i: number) => { 45 | obj.data[i] = [] 46 | Object.entries(data).forEach(([column, value]) => { 47 | if (!obj.headings.includes(column)) { 48 | obj.headings.push(column) 49 | } 50 | obj.data[i].push(value) 51 | }) 52 | }) 53 | } else { 54 | console.warn("That's not valid JSON!") 55 | } 56 | 57 | if (obj) { 58 | return obj 59 | } 60 | } 61 | 62 | return false 63 | } 64 | -------------------------------------------------------------------------------- /src/css/column_filter.css: -------------------------------------------------------------------------------- 1 | .datatable-column-filter-button { 2 | cursor: pointer; 3 | } 4 | 5 | button.datatable-column-filter-button { 6 | padding: 6px 12px; 7 | } 8 | 9 | /* Menu */ 10 | 11 | .datatable-column-filter-wrapper { 12 | position: absolute; 13 | } 14 | 15 | .datatable-column-filter-menu { 16 | background: #fff none repeat scroll 0 0; 17 | border-radius: 3px; 18 | margin: 0; 19 | min-width: 220px; 20 | padding: 5px 0; 21 | box-shadow: 0px 0px 10px 2px #aaa; 22 | } 23 | 24 | .datatable-column-filter-menu > li { 25 | list-style: none; 26 | } 27 | 28 | .datatable-column-filter-menu > li, .datatable-column-filter-menu > li > label { 29 | cursor: pointer; 30 | } 31 | -------------------------------------------------------------------------------- /src/css/editing.css: -------------------------------------------------------------------------------- 1 | .datatable-editor-modal { 2 | position: absolute; 3 | left: 0; 4 | top: 0; 5 | width: 100%; 6 | height: 100%; 7 | background-color: rgba(0, 0, 0, 0.6); 8 | -webkit-animation: 250ms ease 0ms fadeIn; 9 | animation: 250ms ease 0ms fadeIn; 10 | } 11 | 12 | .datatable-editor-modal.closed { 13 | -webkit-animation: 250ms ease 0ms fadeIn; 14 | animation: 250ms ease 0ms fadeIn; 15 | } 16 | 17 | .datatable-editor-modal.closed .datatable-editor-inner { 18 | -webkit-animation: 250ms ease 0ms slideIn; 19 | animation: 250ms ease 0ms slideIn; 20 | } 21 | 22 | .datatable-editor-inner { 23 | width: 30%; 24 | margin: 10% auto; 25 | background-color: #fff; 26 | border-radius: 5px; 27 | -webkit-animation: 250ms ease 0ms slideIn; 28 | animation: 250ms ease 0ms slideIn; 29 | } 30 | 31 | .datatable-editor-header { 32 | position: relative; 33 | border-bottom: 1px solid #ccc; 34 | } 35 | 36 | .datatable-editor-header h4 { 37 | font-size: 20px; 38 | margin: 0; 39 | } 40 | 41 | .datatable-editor-header button { 42 | position: absolute; 43 | right: 10px; 44 | top: 10px; 45 | background-color: transparent; 46 | border: none; 47 | cursor: pointer; 48 | font-size: 24px; 49 | padding: 5px; 50 | line-height: 1; 51 | opacity: 0.6; 52 | } 53 | 54 | .datatable-editor-header button:hover { 55 | opacity: 1; 56 | } 57 | 58 | .datatable-editor-header { 59 | padding: 15px 30px; 60 | } 61 | 62 | .datatable-editor-block { 63 | padding: 15px 60px; 64 | } 65 | 66 | .datatable-editor-row { 67 | margin: 0 0 15px; 68 | } 69 | 70 | .datatable-editor-row:last-child { 71 | margin: 0 0 5px; 72 | } 73 | 74 | .datatable-editor-row:last-child { 75 | text-align: right; 76 | } 77 | 78 | .datatable-editor-label { 79 | width: 25%; 80 | text-align: right; 81 | padding: 0 15px; 82 | } 83 | 84 | .datatable-editor-label, .datatable-editor-input { 85 | display: inline-block; 86 | } 87 | 88 | .datatable-editor-input { 89 | padding: 4px 6px; 90 | border: 1px solid #ccc; 91 | width: 100%; 92 | box-sizing: border-box; 93 | margin: -5px 0; 94 | font-size: inherit; 95 | font-family: inherit; 96 | font-weight: inherit; 97 | color: inherit; 98 | } 99 | 100 | .datatable-editor-row .datatable-editor-input { 101 | margin: 0; 102 | width: 75%; 103 | } 104 | 105 | .datatable-editor-save, .datatable-editor-cancel { 106 | padding: 6px 12px; 107 | font-size: inherit; 108 | font-family: inherit; 109 | font-weight: inherit; 110 | cursor: pointer; 111 | border-radius: 3px; 112 | } 113 | 114 | .datatable-editor-save { 115 | background-color: #27ae60; 116 | border: 1px solid #27ae60; 117 | color: #fff; 118 | } 119 | 120 | .datatable-editor-save:hover { 121 | background-color: #2cc36b; 122 | border-color: #2cc36b; 123 | } 124 | 125 | /* ContextMenu */ 126 | .datatable-editor-wrapper { 127 | position: absolute; 128 | } 129 | 130 | .datatable-editor-menu { 131 | background: #fff none repeat scroll 0 0; 132 | border-radius: 3px; 133 | margin: 0; 134 | min-width: 220px; 135 | padding: 5px 0; 136 | box-shadow: 0px 0px 10px 2px #aaa; 137 | } 138 | 139 | .datatable-editor-menu li { 140 | list-style: none; 141 | } 142 | 143 | .datatable-editor-menu a { 144 | box-sizing: border-box; 145 | color: inherit; 146 | display: block; 147 | padding: 5px 15px; 148 | text-decoration: none; 149 | width: 100%; 150 | } 151 | 152 | .datatable-editor-menu a:hover { 153 | background-color: #ddd; 154 | } 155 | 156 | .datatable-editor-separator { 157 | border-bottom: 1px solid #aaa; 158 | margin: 5px 0; 159 | } 160 | 161 | @-webkit-keyframes fadeIn { 162 | from { 163 | opacity: 0; 164 | } 165 | to { 166 | opacity: 1; 167 | } 168 | } 169 | 170 | @keyframes fadeIn { 171 | from { 172 | opacity: 0; 173 | } 174 | to { 175 | opacity: 1; 176 | } 177 | } 178 | @-webkit-keyframes slideIn { 179 | from { 180 | opacity: 0; 181 | -webkit-transform: translate3d(0, -10%, 0); 182 | transform: translate3d(0, -10%, 0); 183 | } 184 | to { 185 | opacity: 1; 186 | -webkit-transform: translate3d(0, 0%, 0); 187 | transform: translate3d(0, 0%, 0); 188 | } 189 | } 190 | @keyframes slideIn { 191 | from { 192 | opacity: 0; 193 | -webkit-transform: translate3d(0, -10%, 0); 194 | transform: translate3d(0, -10%, 0); 195 | } 196 | to { 197 | opacity: 1; 198 | -webkit-transform: translate3d(0, 0%, 0); 199 | transform: translate3d(0, 0%, 0); 200 | } 201 | } 202 | 203 | .datatable-editor-action .mdi { 204 | margin-right: 5px; 205 | color: #666; 206 | } 207 | -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | .datatable-wrapper.no-header .datatable-container { 2 | border-top: 1px solid #d9d9d9; 3 | } 4 | 5 | .datatable-wrapper.no-footer .datatable-container { 6 | border-bottom: 1px solid #d9d9d9; 7 | } 8 | 9 | .datatable-top, 10 | .datatable-bottom { 11 | padding: 8px 10px; 12 | } 13 | 14 | .datatable-top > nav:first-child, 15 | .datatable-top > div:first-child, 16 | .datatable-bottom > nav:first-child, 17 | .datatable-bottom > div:first-child { 18 | float: left; 19 | } 20 | 21 | .datatable-top > nav:last-child, 22 | .datatable-top > div:not(first-child), 23 | .datatable-bottom > nav:last-child, 24 | .datatable-bottom > div:last-child { 25 | float: right; 26 | } 27 | 28 | .datatable-selector { 29 | padding: 6px; 30 | } 31 | 32 | .datatable-input { 33 | padding: 6px 12px; 34 | } 35 | 36 | .datatable-info { 37 | margin: 7px 0; 38 | } 39 | 40 | /* PAGER */ 41 | .datatable-pagination ul { 42 | margin: 0; 43 | padding-left: 0; 44 | } 45 | 46 | .datatable-pagination li { 47 | list-style: none; 48 | float: left; 49 | } 50 | 51 | .datatable-pagination li.datatable-hidden { 52 | visibility: hidden; 53 | } 54 | 55 | .datatable-pagination a, 56 | .datatable-pagination button { 57 | border: 1px solid transparent; 58 | float: left; 59 | margin-left: 2px; 60 | padding: 6px 12px; 61 | position: relative; 62 | text-decoration: none; 63 | color: #333; 64 | cursor: pointer; 65 | } 66 | 67 | .datatable-pagination a:hover, 68 | .datatable-pagination button:hover { 69 | background-color: #d9d9d9; 70 | } 71 | 72 | .datatable-pagination .datatable-active a, 73 | .datatable-pagination .datatable-active a:focus, 74 | .datatable-pagination .datatable-active a:hover, 75 | .datatable-pagination .datatable-active button, 76 | .datatable-pagination .datatable-active button:focus, 77 | .datatable-pagination .datatable-active button:hover { 78 | background-color: #d9d9d9; 79 | cursor: default; 80 | } 81 | 82 | .datatable-pagination .datatable-ellipsis a, 83 | .datatable-pagination .datatable-disabled a, 84 | .datatable-pagination .datatable-disabled a:focus, 85 | .datatable-pagination .datatable-disabled a:hover, 86 | .datatable-pagination .datatable-ellipsis button, 87 | .datatable-pagination .datatable-disabled button, 88 | .datatable-pagination .datatable-disabled button:focus, 89 | .datatable-pagination .datatable-disabled button:hover { 90 | pointer-events: none; 91 | cursor: default; 92 | } 93 | 94 | .datatable-pagination .datatable-disabled a, 95 | .datatable-pagination .datatable-disabled a:focus, 96 | .datatable-pagination .datatable-disabled a:hover, 97 | .datatable-pagination .datatable-disabled button, 98 | .datatable-pagination .datatable-disabled button:focus, 99 | .datatable-pagination .datatable-disabled button:hover { 100 | cursor: not-allowed; 101 | opacity: 0.4; 102 | } 103 | 104 | .datatable-pagination .datatable-pagination a, 105 | .datatable-pagination .datatable-pagination button { 106 | font-weight: bold; 107 | } 108 | 109 | /* TABLE */ 110 | .datatable-table { 111 | max-width: 100%; 112 | width: 100%; 113 | border-spacing: 0; 114 | border-collapse: separate; 115 | } 116 | 117 | .datatable-table > tbody > tr > td, 118 | .datatable-table > tbody > tr > th, 119 | .datatable-table > tfoot > tr > td, 120 | .datatable-table > tfoot > tr > th, 121 | .datatable-table > thead > tr > td, 122 | .datatable-table > thead > tr > th { 123 | vertical-align: top; 124 | padding: 8px 10px; 125 | } 126 | 127 | .datatable-table > thead > tr > th { 128 | vertical-align: bottom; 129 | text-align: left; 130 | border-bottom: 1px solid #d9d9d9; 131 | } 132 | 133 | .datatable-table > tfoot > tr > th { 134 | vertical-align: bottom; 135 | text-align: left; 136 | border-top: 1px solid #d9d9d9; 137 | } 138 | 139 | .datatable-table th { 140 | vertical-align: bottom; 141 | text-align: left; 142 | } 143 | 144 | .datatable-table th a { 145 | text-decoration: none; 146 | color: inherit; 147 | } 148 | 149 | .datatable-table th button, 150 | .datatable-pagination-list button { 151 | color: inherit; 152 | border: 0; 153 | background-color: inherit; 154 | cursor: pointer; 155 | text-align: inherit; 156 | font-family: inherit; 157 | font-weight: inherit; 158 | font-size: inherit; 159 | } 160 | 161 | .datatable-sorter, .datatable-filter { 162 | display: inline-block; 163 | height: 100%; 164 | position: relative; 165 | width: 100%; 166 | } 167 | 168 | .datatable-sorter::before, 169 | .datatable-sorter::after { 170 | content: ""; 171 | height: 0; 172 | width: 0; 173 | position: absolute; 174 | right: 4px; 175 | border-left: 4px solid transparent; 176 | border-right: 4px solid transparent; 177 | opacity: 0.2; 178 | } 179 | 180 | 181 | .datatable-sorter::before { 182 | border-top: 4px solid #000; 183 | bottom: 0px; 184 | } 185 | 186 | .datatable-sorter::after { 187 | border-bottom: 4px solid #000; 188 | border-top: 4px solid transparent; 189 | top: 0px; 190 | } 191 | 192 | .datatable-ascending .datatable-sorter::after, 193 | .datatable-descending .datatable-sorter::before, 194 | .datatable-ascending .datatable-filter::after, 195 | .datatable-descending .datatable-filter::before { 196 | opacity: 0.6; 197 | } 198 | 199 | .datatable-filter::before { 200 | content: ""; 201 | position: absolute; 202 | right: 4px; 203 | opacity: 0.2; 204 | width: 0; 205 | height: 0; 206 | border-left: 7px solid transparent; 207 | border-right: 7px solid transparent; 208 | border-radius: 50%; 209 | border-top: 10px solid #000; 210 | top: 25%; 211 | } 212 | 213 | .datatable-filter-active .datatable-filter::before { 214 | opacity: 0.6; 215 | } 216 | 217 | .datatable-empty { 218 | text-align: center; 219 | } 220 | 221 | .datatable-top::after, .datatable-bottom::after { 222 | clear: both; 223 | content: " "; 224 | display: table; 225 | } 226 | 227 | table.datatable-table:focus tr.datatable-cursor > td:first-child { 228 | border-left: 3px blue solid; 229 | } 230 | 231 | table.datatable-table:focus { 232 | outline: solid 1px black; 233 | outline-offset: -1px; 234 | } 235 | -------------------------------------------------------------------------------- /src/date.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs" 2 | import customParseFormat from "dayjs/plugin/customParseFormat" 3 | 4 | dayjs.extend(customParseFormat) 5 | 6 | /** 7 | * Use dayjs to parse cell contents for sorting 8 | */ 9 | export const parseDate = (content: string, format: string) => { 10 | let date: number | string 11 | 12 | // Converting to YYYYMMDD ensures we can accurately sort the column numerically 13 | 14 | if (format) { 15 | switch (format) { 16 | case "ISO_8601": 17 | // ISO8601 is already lexiographically sorted, so we can just sort it as a string. 18 | date = content 19 | break 20 | case "RFC_2822": 21 | date = dayjs(content.slice(5), "DD MMM YYYY HH:mm:ss ZZ").unix() 22 | break 23 | case "MYSQL": 24 | date = dayjs(content, "YYYY-MM-DD hh:mm:ss").unix() 25 | break 26 | case "UNIX": 27 | date = dayjs(content).unix() 28 | break 29 | // User defined format using the data-format attribute or columns[n].format option 30 | default: 31 | date = dayjs(content, format, true).valueOf() 32 | break 33 | } 34 | } 35 | return date 36 | } 37 | -------------------------------------------------------------------------------- /src/editing/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default config 3 | * @type {Object} 4 | */ 5 | import {Editor} from "./editor" 6 | 7 | export const defaultConfig = { 8 | classes: { 9 | row: "datatable-editor-row", 10 | form: "datatable-editor-form", 11 | item: "datatable-editor-item", 12 | menu: "datatable-editor-menu", 13 | save: "datatable-editor-save", 14 | block: "datatable-editor-block", 15 | cancel: "datatable-editor-cancel", 16 | close: "datatable-editor-close", 17 | inner: "datatable-editor-inner", 18 | input: "datatable-editor-input", 19 | label: "datatable-editor-label", 20 | modal: "datatable-editor-modal", 21 | action: "datatable-editor-action", 22 | header: "datatable-editor-header", 23 | wrapper: "datatable-editor-wrapper", 24 | editable: "datatable-editor-editable", 25 | container: "datatable-editor-container", 26 | separator: "datatable-editor-separator" 27 | }, 28 | 29 | labels: { 30 | closeX: "x", 31 | editCell: "Edit Cell", 32 | editRow: "Edit Row", 33 | removeRow: "Remove Row", 34 | reallyRemove: "Are you sure?", 35 | reallyCancel: "Do you really want to cancel?", 36 | save: "Save", 37 | cancel: "Cancel" 38 | }, 39 | 40 | cancelModal: editor => confirm(editor.options.labels.reallyCancel), 41 | 42 | // edit inline instead of using a modal lay-over for editing content 43 | inline: true, 44 | 45 | // include hidden columns in the editor 46 | hiddenColumns: false, 47 | 48 | // enable the context menu 49 | contextMenu: true, 50 | 51 | // event to start editing 52 | clickEvent: "dblclick", 53 | 54 | // indexes of columns not to be edited 55 | excludeColumns: [], 56 | 57 | // set the context menu items 58 | menuItems: [ 59 | { 60 | text: (editor: Editor) => editor.options.labels.editCell, 61 | action: (editor: Editor, _event: Event) => { 62 | if (!(editor.event.target instanceof Element)) { 63 | return 64 | } 65 | const cell = editor.event.target.closest("td") 66 | return editor.editCell(cell) 67 | } 68 | }, 69 | { 70 | text: (editor: Editor) => editor.options.labels.editRow, 71 | action: (editor: Editor, _event: Event) => { 72 | if (!(editor.event.target instanceof Element)) { 73 | return 74 | } 75 | const row = editor.event.target.closest("tr") 76 | return editor.editRow(row) 77 | } 78 | }, 79 | { 80 | separator: true 81 | }, 82 | { 83 | text: (editor: Editor) => editor.options.labels.removeRow, 84 | action: (editor: Editor, _event: Event) => { 85 | if (!(editor.event.target instanceof Element)) { 86 | return 87 | } 88 | if (confirm(editor.options.labels.reallyRemove)) { 89 | const row = editor.event.target.closest("tr") 90 | editor.removeRow(row) 91 | } 92 | } 93 | } 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /src/editing/index.ts: -------------------------------------------------------------------------------- 1 | export {makeEditable} from "./editor" 2 | -------------------------------------------------------------------------------- /src/editing/types.ts: -------------------------------------------------------------------------------- 1 | import {cellType} from "../types" 2 | import {Editor} from "./editor" 3 | 4 | 5 | interface dataType { 6 | cell?: cellType; 7 | rowIndex?: number; 8 | columnIndex?: number; 9 | content?: string; 10 | input?: HTMLInputElement; 11 | row?: cellType[]; 12 | inputs?: HTMLInputElement[]; 13 | } 14 | 15 | type menuItemType = { 16 | text?: (editor: Editor) => string, 17 | action?: (editor: Editor, event: Event) => void, 18 | separator?: boolean, 19 | url?: string, 20 | } 21 | 22 | interface EditorOptions { 23 | classes?: { 24 | row?: string, 25 | form?: string, 26 | item?: string, 27 | menu?: string, 28 | save?: string, 29 | block?: string, 30 | cancel?: string, 31 | close?: string, 32 | inner?: string, 33 | input?: string, 34 | label?: string, 35 | modal?: string, 36 | action?: string, 37 | header?: string, 38 | wrapper?: string, 39 | editable?: string, 40 | container?: string, 41 | separator?: string, 42 | }, 43 | labels?: { 44 | cancel?: string, 45 | closeX?: string, 46 | editCell?: string, 47 | editRow?: string, 48 | removeRow?: string, 49 | reallyCancel?: string, 50 | reallyRemove?: string, 51 | save?: string 52 | } 53 | 54 | cancelModal?: (editor: Editor) => boolean, 55 | 56 | // include hidden columns in the editor 57 | hiddenColumns?: boolean, 58 | 59 | // edit inline instead of using a modal lay-over for editing content 60 | inline?: boolean, 61 | 62 | // enable the context menu 63 | contextMenu?: boolean, 64 | 65 | // event to start editing 66 | clickEvent?: string, 67 | 68 | // indexes of columns not to be edited 69 | excludeColumns?: number[], 70 | menuItems?: menuItemType[] 71 | } 72 | 73 | export { 74 | dataType, 75 | menuItemType, 76 | EditorOptions 77 | } 78 | -------------------------------------------------------------------------------- /src/export/csv.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cellToText, 3 | isObject 4 | } from "../helpers" 5 | import {DataTable} from "../datatable" 6 | import { 7 | cellDataType, 8 | cellType, 9 | dataRowType, 10 | headerCellType 11 | } from "../types" 12 | 13 | /** 14 | * Export table to CSV 15 | */ 16 | 17 | interface csvUserOptions { 18 | download?: boolean, 19 | skipColumn?: number[], 20 | lineDelimiter?: string, 21 | columnDelimiter?: string, 22 | selection?: number | number[], 23 | filename?: string, 24 | } 25 | 26 | 27 | export const exportCSV = function(dt: DataTable, userOptions: csvUserOptions = {}) { 28 | if (!dt.hasHeadings && !dt.hasRows) return false 29 | 30 | const defaults = { 31 | download: true, 32 | skipColumn: [], 33 | lineDelimiter: "\n", 34 | columnDelimiter: "," 35 | } 36 | 37 | // Check for the options object 38 | if (!isObject(userOptions)) { 39 | return false 40 | } 41 | 42 | const options = { 43 | ...defaults, 44 | ...userOptions 45 | } 46 | const columnShown = (index: number) => !options.skipColumn.includes(index) && !dt.columns.settings[index]?.hidden 47 | const headers = dt.data.headings.filter((_heading: headerCellType, index: number) => columnShown(index)).map((header: headerCellType) => header.text ?? header.data) 48 | 49 | // Selection or whole table 50 | let selectedRows: dataRowType[] 51 | if (options.selection) { 52 | // Page number 53 | if (Array.isArray(options.selection)) { 54 | // Array of page numbers 55 | selectedRows = [] 56 | for (let i = 0; i < options.selection.length; i++) { 57 | selectedRows = selectedRows.concat(dt.pages[options.selection[i] - 1].map(row => row.row)) 58 | } 59 | 60 | } else { 61 | selectedRows = dt.pages[options.selection - 1].map(row => row.row) 62 | } 63 | } else { 64 | selectedRows = dt.data.data 65 | } 66 | 67 | let rows : cellDataType[][] = [] 68 | // Include headings 69 | rows[0] = headers 70 | rows = rows.concat(selectedRows.map((row: dataRowType) => { 71 | const shownCells = row.cells.filter((_cell: cellType, index: number) => columnShown(index)) 72 | return shownCells.map((cell: cellType) => cellToText(cell)) 73 | })) 74 | 75 | // Only proceed if we have data 76 | if (rows.length) { 77 | let str = "" 78 | rows.forEach(row => { 79 | row.forEach((cell: cellDataType) => { 80 | if (typeof cell === "string") { 81 | cell = cell.trim() 82 | cell = cell.replace(/\s{2,}/g, " ") 83 | cell = cell.replace(/\n/g, " ") 84 | cell = cell.replace(/"/g, "\"\"") 85 | //have to manually encode "#" as encodeURI leaves it as is. 86 | cell = cell.replace(/#/g, "%23") 87 | if (cell.includes(",")) { 88 | cell = `"${cell}"` 89 | } 90 | } 91 | str += cell + options.columnDelimiter 92 | }) 93 | // Remove trailing column delimiter 94 | str = str.trim().substring(0, str.length - 1) 95 | 96 | // Apply line delimiter 97 | str += options.lineDelimiter 98 | }) 99 | 100 | // Remove trailing line delimiter 101 | str = str.trim().substring(0, str.length - 1) 102 | 103 | // Download 104 | if (options.download) { 105 | // Create a link to trigger the download 106 | const link = document.createElement("a") 107 | link.href = encodeURI(`data:text/csv;charset=utf-8,${str}`) 108 | link.download = `${options.filename || "datatable_export"}.csv` 109 | 110 | // Append the link 111 | document.body.appendChild(link) 112 | 113 | // Trigger the download 114 | link.click() 115 | 116 | // Remove the link 117 | document.body.removeChild(link) 118 | } 119 | 120 | return str 121 | } 122 | 123 | return false 124 | } 125 | -------------------------------------------------------------------------------- /src/export/index.ts: -------------------------------------------------------------------------------- 1 | export {exportCSV} from "./csv" 2 | export {exportJSON} from "./json" 3 | export {exportSQL} from "./sql" 4 | export {exportTXT} from "./txt" 5 | -------------------------------------------------------------------------------- /src/export/json.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cellToText, 3 | isObject 4 | } from "../helpers" 5 | import {DataTable} from "../datatable" 6 | import { 7 | cellDataType, 8 | cellType, 9 | dataRowType, 10 | headerCellType 11 | } from "../types" 12 | /** 13 | * Export table to JSON 14 | */ 15 | 16 | interface jsonUserOptions { 17 | download?: boolean, 18 | skipColumn?: number[], 19 | replacer?: null | ((key, value) => string) | (string | number)[], 20 | space?: number, 21 | selection?: number | number[], 22 | filename?: string, 23 | } 24 | 25 | 26 | export const exportJSON = function(dt: DataTable, userOptions: jsonUserOptions = {}) { 27 | if (!dt.hasHeadings && !dt.hasRows) return false 28 | 29 | 30 | const defaults = { 31 | download: true, 32 | skipColumn: [], 33 | replacer: null, 34 | space: 4 35 | } 36 | 37 | // Check for the options object 38 | if (!isObject(userOptions)) { 39 | return false 40 | } 41 | 42 | const options = { 43 | ...defaults, 44 | ...userOptions 45 | } 46 | 47 | const columnShown = (index: number) => !options.skipColumn.includes(index) && !dt.columns.settings[index]?.hidden 48 | 49 | // Selection or whole table 50 | let selectedRows: dataRowType[] 51 | if (options.selection) { 52 | // Page number 53 | if (Array.isArray(options.selection)) { 54 | // Array of page numbers 55 | selectedRows = [] 56 | for (let i = 0; i < options.selection.length; i++) { 57 | selectedRows = selectedRows.concat(dt.pages[options.selection[i] - 1].map(row => row.row)) 58 | } 59 | } else { 60 | selectedRows = dt.pages[options.selection - 1].map(row => row.row) 61 | } 62 | } else { 63 | selectedRows = dt.data.data 64 | } 65 | 66 | const rows: cellDataType[][] = selectedRows.map((row: dataRowType) => { 67 | const shownCells = row.cells.filter((_cell: cellType, index: number) => columnShown(index)) 68 | return shownCells.map((cell: cellType) => cellToText(cell)) 69 | }) 70 | 71 | const headers = dt.data.headings.filter((_heading: headerCellType, index: number) => columnShown(index)).map((header: headerCellType) => header.text ?? String(header.data)) 72 | 73 | // Only proceed if we have data 74 | if (rows.length) { 75 | const arr: (void | { [key: string]: cellDataType})[] = [] 76 | rows.forEach((row: cellDataType[], x: number) => { 77 | arr[x] = arr[x] || {} 78 | row.forEach((cell: cellDataType, i: number) => { 79 | arr[x][headers[i]] = cell 80 | }) 81 | }) 82 | 83 | // Convert the array of objects to JSON string 84 | const str = JSON.stringify(arr, options.replacer, options.space) 85 | 86 | // Download 87 | if (options.download) { 88 | // Create a link to trigger the download 89 | 90 | const blob = new Blob( 91 | [str], 92 | { 93 | type: "data:application/json;charset=utf-8" 94 | } 95 | ) 96 | const url = URL.createObjectURL(blob) 97 | 98 | 99 | const link = document.createElement("a") 100 | link.href = url 101 | link.download = `${options.filename || "datatable_export"}.json` 102 | 103 | // Append the link 104 | document.body.appendChild(link) 105 | 106 | // Trigger the download 107 | link.click() 108 | 109 | // Remove the link 110 | document.body.removeChild(link) 111 | URL.revokeObjectURL(url) 112 | } 113 | 114 | return str 115 | } 116 | 117 | return false 118 | } 119 | -------------------------------------------------------------------------------- /src/export/sql.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cellToText, 3 | isObject 4 | } from "../helpers" 5 | import {DataTable} from "../datatable" 6 | import { 7 | cellDataType, 8 | cellType, 9 | dataRowType, 10 | headerCellType 11 | } from "../types" 12 | /** 13 | * Export table to SQL 14 | */ 15 | 16 | interface sqlUserOptions { 17 | download?: boolean, 18 | skipColumn?: number[], 19 | tableName?: string, 20 | selection?: number | number[], 21 | filename?: string, 22 | } 23 | 24 | export const exportSQL = function(dt: DataTable, userOptions : sqlUserOptions = {}) { 25 | if (!dt.hasHeadings && !dt.hasRows) return false 26 | 27 | const defaults = { 28 | download: true, 29 | skipColumn: [], 30 | tableName: "myTable" 31 | } 32 | 33 | // Check for the options object 34 | if (!isObject(userOptions)) { 35 | return false 36 | } 37 | 38 | const options = { 39 | ...defaults, 40 | ...userOptions 41 | } 42 | const columnShown = (index: number) => !options.skipColumn.includes(index) && !dt.columns.settings[index]?.hidden 43 | 44 | // Selection or whole table 45 | let selectedRows: dataRowType[] = [] 46 | if (options.selection) { 47 | // Page number 48 | if (Array.isArray(options.selection)) { 49 | // Array of page numbers 50 | for (let i = 0; i < options.selection.length; i++) { 51 | selectedRows = selectedRows.concat(dt.pages[options.selection[i] - 1].map(row => row.row)) 52 | } 53 | 54 | } else { 55 | selectedRows = dt.pages[options.selection - 1].map(row => row.row) 56 | } 57 | } else { 58 | selectedRows = dt.data.data 59 | } 60 | 61 | const rows: cellDataType[][] = selectedRows.map((row: dataRowType) => { 62 | const shownCells = row.cells.filter((_cell: cellType, index: number) => columnShown(index)) 63 | return shownCells.map((cell: cellType) => cellToText(cell)) 64 | }) 65 | 66 | const headers = dt.data.headings.filter((_heading: headerCellType, index: number) => columnShown(index)).map((header: headerCellType) => header.text ?? String(header.data)) 67 | // Only proceed if we have data 68 | if (rows.length) { 69 | // Begin INSERT statement 70 | let str = `INSERT INTO \`${options.tableName}\` (` 71 | 72 | // Convert table headings to column names 73 | headers.forEach((header: string) => { 74 | str += `\`${header}\`,` 75 | }) 76 | 77 | // Remove trailing comma 78 | str = str.trim().substring(0, str.length - 1) 79 | 80 | // Begin VALUES 81 | str += ") VALUES " 82 | 83 | // Iterate rows and convert cell data to column values 84 | 85 | rows.forEach((row: cellDataType[]) => { 86 | str += "(" 87 | row.forEach((cell: cellDataType) => { 88 | if (typeof cell === "string") { 89 | str += `"${cell}",` 90 | } else { 91 | str += `${cell},` 92 | } 93 | }) 94 | // Remove trailing comma 95 | str = str.trim().substring(0, str.length - 1) 96 | 97 | // end VALUES 98 | str += ")," 99 | 100 | }) 101 | 102 | // Remove trailing comma 103 | str = str.trim().substring(0, str.length - 1) 104 | 105 | // Add trailing colon 106 | str += ";" 107 | 108 | if (options.download) { 109 | str = `data:application/sql;charset=utf-8,${str}` 110 | } 111 | 112 | // Download 113 | if (options.download) { 114 | // Create a link to trigger the download 115 | const link = document.createElement("a") 116 | link.href = encodeURI(str) 117 | link.download = `${options.filename || "datatable_export"}.sql` 118 | 119 | // Append the link 120 | document.body.appendChild(link) 121 | 122 | // Trigger the download 123 | link.click() 124 | 125 | // Remove the link 126 | document.body.removeChild(link) 127 | } 128 | 129 | return str 130 | } 131 | 132 | return false 133 | } 134 | -------------------------------------------------------------------------------- /src/export/txt.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cellToText, 3 | isObject 4 | } from "../helpers" 5 | import {DataTable} from "../datatable" 6 | import { 7 | cellDataType, 8 | cellType, 9 | dataRowType, 10 | headerCellType 11 | } from "../types" 12 | /** 13 | * Export table to TXT 14 | */ 15 | interface txtUserOptions { 16 | download?: boolean, 17 | skipColumn?: number[], 18 | lineDelimiter?: string, 19 | columnDelimiter?: string, 20 | selection?: number | number[], 21 | filename?: string, 22 | } 23 | 24 | 25 | export const exportTXT = function(dt: DataTable, userOptions : txtUserOptions = {}) { 26 | if (!dt.hasHeadings && !dt.hasRows) return false 27 | 28 | const defaults = { 29 | download: true, 30 | skipColumn: [], 31 | lineDelimiter: "\n", 32 | columnDelimiter: "," 33 | } 34 | 35 | // Check for the options object 36 | if (!isObject(userOptions)) { 37 | return false 38 | } 39 | 40 | const options = { 41 | ...defaults, 42 | ...userOptions 43 | } 44 | 45 | const columnShown = (index: number) => !options.skipColumn.includes(index) && !dt.columns.settings[index]?.hidden 46 | 47 | const headers = dt.data.headings.filter((_heading: headerCellType, index: number) => columnShown(index)).map((header: headerCellType) => header.text ?? header.data) 48 | 49 | // Selection or whole table 50 | let selectedRows: dataRowType[] 51 | if (options.selection) { 52 | // Page number 53 | if (Array.isArray(options.selection)) { 54 | // Array of page numbers 55 | selectedRows = [] 56 | for (let i = 0; i < options.selection.length; i++) { 57 | selectedRows = selectedRows.concat(dt.pages[options.selection[i] - 1].map(row => row.row)) 58 | } 59 | } else { 60 | selectedRows = dt.pages[options.selection - 1].map(row => row.row) 61 | } 62 | } else { 63 | selectedRows = dt.data.data 64 | } 65 | 66 | let rows : cellDataType[][] = [] 67 | // Include headings 68 | rows[0] = headers 69 | rows = rows.concat(selectedRows.map((row: dataRowType) => { 70 | const shownCells = row.cells.filter((_cell: cellType, index: number) => columnShown(index)) 71 | return shownCells.map((cell: cellType) => cellToText(cell)) 72 | })) 73 | 74 | // Only proceed if we have data 75 | if (rows.length) { 76 | let str = "" 77 | 78 | rows.forEach(row => { 79 | row.forEach((cell: cellDataType) => { 80 | if (typeof cell === "string") { 81 | cell = cell.trim() 82 | cell = cell.replace(/\s{2,}/g, " ") 83 | cell = cell.replace(/\n/g, " ") 84 | cell = cell.replace(/"/g, "\"\"") 85 | //have to manually encode "#" as encodeURI leaves it as is. 86 | cell = cell.replace(/#/g, "%23") 87 | if (cell.includes(",")) { 88 | cell = `"${cell}"` 89 | } 90 | } 91 | str += cell + options.columnDelimiter 92 | }) 93 | // Remove trailing column delimiter 94 | str = str.trim().substring(0, str.length - 1) 95 | 96 | // Apply line delimiter 97 | str += options.lineDelimiter 98 | 99 | }) 100 | 101 | // Remove trailing line delimiter 102 | str = str.trim().substring(0, str.length - 1) 103 | 104 | if (options.download) { 105 | str = `data:text/csv;charset=utf-8,${str}` 106 | } 107 | // Download 108 | if (options.download) { 109 | // Create a link to trigger the download 110 | const link = document.createElement("a") 111 | link.href = encodeURI(str) 112 | link.download = `${options.filename || "datatable_export"}.txt` 113 | 114 | // Append the link 115 | document.body.appendChild(link) 116 | 117 | // Trigger the download 118 | link.click() 119 | 120 | // Remove the link 121 | document.body.removeChild(link) 122 | } 123 | 124 | return str 125 | } 126 | 127 | return false 128 | } 129 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cellDataType, 3 | cellType, 4 | columnSettingsType, 5 | inputCellType, 6 | nodeType, 7 | textNodeType 8 | } from "./types" 9 | 10 | /** 11 | * Check is item is object 12 | */ 13 | export const isObject = (val: (string | number | boolean | object | null | undefined )) => Object.prototype.toString.call(val) === "[object Object]" 14 | 15 | /** 16 | * Check for valid JSON string 17 | */ 18 | export const isJson = (str: string) => { 19 | let t = !1 20 | try { 21 | t = JSON.parse(str) 22 | } catch (e) { 23 | return !1 24 | } 25 | return !(null === t || (!Array.isArray(t) && !isObject(t))) && t 26 | } 27 | 28 | /** 29 | * Create DOM element node 30 | */ 31 | export const createElement = (nodeName: string, attrs?: { [key: string]: string}) => { 32 | const dom = document.createElement(nodeName) 33 | if (attrs && "object" == typeof attrs) { 34 | for (const attr in attrs) { 35 | if ("html" === attr) { 36 | dom.innerHTML = attrs[attr] 37 | } else { 38 | dom.setAttribute(attr, attrs[attr]) 39 | } 40 | } 41 | } 42 | return dom 43 | } 44 | 45 | export const objToText = (obj: nodeType) => { 46 | if (["#text", "#comment"].includes(obj.nodeName)) { 47 | return (obj as textNodeType).data 48 | } 49 | if (obj.childNodes) { 50 | return obj.childNodes.map((childNode: nodeType) => objToText(childNode)).join("") 51 | } 52 | return "" 53 | } 54 | 55 | export const cellToText = (obj: inputCellType | cellDataType | null | undefined): string => { 56 | if (obj === null || obj === undefined) { 57 | return "" 58 | } else if (obj.hasOwnProperty("text") || obj.hasOwnProperty("data")) { 59 | const cell = obj as cellType 60 | return cell.text ?? cellToText(cell.data) 61 | } else if (obj.hasOwnProperty("nodeName")) { 62 | return objToText(obj as nodeType) 63 | } 64 | return String(obj) 65 | } 66 | 67 | 68 | export const escapeText = function(text: string) { 69 | return text 70 | .replace(/&/g, "&") 71 | .replace(//g, ">") 73 | .replace(/"/g, """) 74 | } 75 | 76 | 77 | export const visibleToColumnIndex = function(visibleIndex: number, columns: columnSettingsType[]) { 78 | let counter = 0 79 | let columnIndex = 0 80 | while (counter < (visibleIndex+1)) { 81 | const columnSettings = columns[columnIndex] 82 | if (!columnSettings.hidden) { 83 | counter += 1 84 | } 85 | columnIndex += 1 86 | } 87 | return columnIndex-1 88 | } 89 | 90 | export const columnToVisibleIndex = function(columnIndex: number, columns: columnSettingsType[]) { 91 | let visibleIndex = columnIndex 92 | let counter = 0 93 | while (counter < columnIndex) { 94 | const columnSettings = columns[counter] 95 | if (columnSettings.hidden) { 96 | visibleIndex -= 1 97 | } 98 | counter++ 99 | } 100 | return visibleIndex 101 | } 102 | 103 | /** 104 | * Converts a [NamedNodeMap](https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap) into a normal object. 105 | * 106 | * @param map The `NamedNodeMap` to convert 107 | */ 108 | export const namedNodeMapToObject = function(map: NamedNodeMap) { 109 | const obj = {} 110 | if (map) { 111 | for (const attr of map) { 112 | obj[attr.name] = attr.value 113 | } 114 | } 115 | return obj 116 | } 117 | 118 | /** 119 | * Convert class names to a CSS selector. Multiple classes should be separated by spaces. 120 | * Examples: 121 | * - "my-class" -> ".my-class" 122 | * - "my-class second-class" -> ".my-class.second-class" 123 | * 124 | * @param classNames The class names to convert. Can contain multiple classes separated by spaces. 125 | */ 126 | export const classNamesToSelector = (classNames: string) => { 127 | if (!classNames) { 128 | return null 129 | } 130 | return classNames.trim().split(" ").map(className => `.${className}`).join("") 131 | } 132 | 133 | /** 134 | * Check if the element contains all the classes. Multiple classes should be separated by spaces. 135 | * 136 | * @param element The element that will be checked 137 | * @param classes The classes that must be present in the element. Can contain multiple classes separated by spaces. 138 | */ 139 | export const containsClass = (element: Element, classes: string) => { 140 | const hasMissingClass = classes?.split(" ").some(className => !element.classList.contains(className)) 141 | return !hasMissingClass 142 | } 143 | 144 | /** 145 | * Join two strings with spaces. Null values are ignored. 146 | * Examples: 147 | * - joinWithSpaces("a", "b") -> "a b" 148 | * - joinWithSpaces("a", null) -> "a" 149 | * - joinWithSpaces(null, "b") -> "b" 150 | * - joinWithSpaces("a", "b c") -> "a b c" 151 | * 152 | * @param first The first string to join 153 | * @param second The second string to join 154 | */ 155 | export const joinWithSpaces = (first: string | null | undefined, second: string | null | undefined) => { 156 | if (first) { 157 | if (second) { 158 | return `${first} ${second}` 159 | } 160 | return first 161 | } else if (second) { 162 | return second 163 | } 164 | return "" 165 | } 166 | 167 | // Source: https://www.freecodecamp.org/news/javascript-debounce-example/ 168 | 169 | export const debounce = function(func: () => void, timeout = 300) { 170 | let timer: number 171 | return (..._args: any[]) => { 172 | clearTimeout(timer) 173 | timer = window.setTimeout(() => func(), timeout) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* simple-datatables 2 | * Copyright (c) 2015-2017 Karl Saunders (https://mobius.ovh) 3 | * Copyright (c) 2018- Johannes Wilm (https://www.johanneswilm.org) 4 | * Licensed under LGPL (https://opensource.org/licenses/lgpl-license) 5 | */ 6 | export {DataTable} from "./datatable" 7 | export {convertCSV, convertJSON} from "./convert" 8 | export {exportCSV, exportJSON, exportSQL, exportTXT} from "./export" 9 | export {createElement, isJson, isObject} from "./helpers" 10 | export {makeEditable} from "./editing" 11 | export {addColumnFilter} from "./column_filter" 12 | -------------------------------------------------------------------------------- /src/rows.ts: -------------------------------------------------------------------------------- 1 | import {readDataCell} from "./read_data" 2 | import {DataTable} from "./datatable" 3 | import {cellType, dataRowType, inputCellType} from "./types" 4 | import {cellToText, classNamesToSelector} from "./helpers" 5 | 6 | /** 7 | * Rows API 8 | */ 9 | export class Rows { 10 | cursor: (false | number) 11 | 12 | dt: DataTable 13 | 14 | constructor(dt: DataTable) { 15 | this.dt = dt 16 | 17 | this.cursor = false 18 | } 19 | 20 | setCursor(index: (false | number) = false) { 21 | if (index === this.cursor) { 22 | return 23 | } 24 | const oldCursor = this.cursor 25 | this.cursor = index 26 | this.dt._renderTable() 27 | if (index !== false && this.dt.options.scrollY) { 28 | const cursorSelector = classNamesToSelector(this.dt.options.classes.cursor) 29 | const cursorDOM = this.dt.dom.querySelector(`tr${cursorSelector}`) 30 | if (cursorDOM) { 31 | cursorDOM.scrollIntoView({block: "nearest"}) 32 | } 33 | } 34 | this.dt.emit("datatable.cursormove", this.cursor, oldCursor) 35 | } 36 | 37 | /** 38 | * Add new row 39 | */ 40 | add(data: cellType[]) { 41 | if (!Array.isArray(data) || data.length < 1) { 42 | return 43 | } 44 | 45 | const row: dataRowType = { 46 | cells: data.map((cell: cellType, index: number) => { 47 | const columnSettings = this.dt.columns.settings[index] 48 | return readDataCell(cell, columnSettings) 49 | }) 50 | } 51 | this.dt.data.data.push(row) 52 | this.dt.hasRows = true 53 | this.dt.update(true) 54 | } 55 | 56 | /** 57 | * Remove row(s) 58 | */ 59 | remove(select: number | number[]) { 60 | if (Array.isArray(select)) { 61 | this.dt.data.data = this.dt.data.data.filter((_row: dataRowType, index: number) => !select.includes(index)) 62 | // We may have emptied the table 63 | if ( !this.dt.data.data.length ) { 64 | this.dt.hasRows = false 65 | } 66 | this.dt.update(true) 67 | } else { 68 | return this.remove([select]) 69 | } 70 | } 71 | 72 | 73 | /** 74 | * Find index of row by searching for a value in a column 75 | */ 76 | findRowIndex(columnIndex: number, value: string | boolean | number) { 77 | // returns row index of first case-insensitive string match 78 | // inside the td innerText at specific column index 79 | return this.dt.data.data.findIndex( 80 | (row: dataRowType) => { 81 | const cell = row.cells[columnIndex] 82 | const cellText = cellToText(cell) 83 | return cellText.toLowerCase().includes(String(value).toLowerCase()) 84 | } 85 | ) 86 | } 87 | 88 | /** 89 | * Find index, row, and column data by searching for a value in a column 90 | */ 91 | findRow(columnIndex: number, value: string | boolean | number) { 92 | // get the row index 93 | const index = this.findRowIndex(columnIndex, value) 94 | // exit if not found 95 | if (index < 0) { 96 | return { 97 | index: -1, 98 | row: null, 99 | cols: [] 100 | } 101 | } 102 | // get the row from data 103 | const row = this.dt.data.data[index] 104 | // return innerHTML of each td 105 | const cols = row.cells.map((cell: cellType) => cell.data) 106 | // return everything 107 | return { 108 | index, 109 | row, 110 | cols 111 | } 112 | } 113 | 114 | /** 115 | * Update a row with new data 116 | */ 117 | updateRow(select: number, data: inputCellType[]) { 118 | const row: dataRowType = { 119 | cells: data.map((cell: inputCellType, index: number) => { 120 | const columnSettings = this.dt.columns.settings[index] 121 | return readDataCell(cell, columnSettings) 122 | }) 123 | } 124 | this.dt.data.data.splice(select, 1, row) 125 | this.dt.update(true) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/templates.ts: -------------------------------------------------------------------------------- 1 | // Template for custom layouts 2 | export const layoutTemplate = (options, dom) => `
    3 | ${ 4 | options.paging && options.perPageSelect ? 5 | `
    6 | 9 |
    ` : 10 | "" 11 | } 12 | ${ 13 | options.searchable ? 14 | `
    15 | 16 |
    ` : 17 | "" 18 | } 19 |
    20 |
    21 |
    22 | ${ 23 | options.paging ? 24 | `
    ` : 25 | "" 26 | } 27 | 28 |
    ` 29 | -------------------------------------------------------------------------------- /src/virtual_pager_dom.ts: -------------------------------------------------------------------------------- 1 | import {DataTableConfiguration, elementNodeType} from "./types" 2 | 3 | /** 4 | * Pager truncation algorithm 5 | */ 6 | const truncate = (paginationListItems: elementNodeType[], currentPage: number, pagesLength: number, options: DataTableConfiguration) : elementNodeType[] => { 7 | const pagerDelta = options.pagerDelta 8 | const classes = options.classes 9 | const ellipsisText = options.ellipsisText 10 | 11 | const doublePagerDelta = 2 * pagerDelta 12 | let previousPage = currentPage - pagerDelta 13 | let nextPage = currentPage + pagerDelta 14 | 15 | if (currentPage < 4 - pagerDelta + doublePagerDelta) { 16 | nextPage = 3 + doublePagerDelta 17 | } else if (currentPage > pagesLength - (3 - pagerDelta + doublePagerDelta)) { 18 | previousPage = pagesLength - (2 + doublePagerDelta) 19 | } 20 | const paginationListItemsToModify: elementNodeType[] = [] 21 | for (let k = 1; k <= pagesLength; k++) { 22 | if (1 == k || k == pagesLength || (k >= previousPage && k <= nextPage)) { 23 | const li = paginationListItems[k - 1] 24 | paginationListItemsToModify.push(li) 25 | } 26 | } 27 | let previousLi: elementNodeType 28 | const modifiedLis: elementNodeType[] = [] 29 | paginationListItemsToModify.forEach(li => { 30 | const pageNumber = parseInt((li.childNodes[0] as elementNodeType).attributes["data-page"], 10) 31 | if (previousLi) { 32 | const previousPageNumber = parseInt((previousLi.childNodes[0] as elementNodeType).attributes["data-page"], 10) 33 | if (pageNumber - previousPageNumber == 2) { 34 | modifiedLis.push(paginationListItems[previousPageNumber]) 35 | } else if (pageNumber - previousPageNumber != 1) { 36 | const newLi: elementNodeType = { 37 | nodeName: "LI", 38 | attributes: { 39 | class: `${classes.paginationListItem} ${classes.ellipsis} ${classes.disabled}` 40 | }, 41 | childNodes: [ 42 | { 43 | nodeName: "BUTTON", 44 | attributes: { 45 | class: classes.paginationListItemLink 46 | }, 47 | childNodes: [ 48 | { 49 | nodeName: "#text", 50 | data: ellipsisText 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | modifiedLis.push(newLi) 57 | } 58 | } 59 | modifiedLis.push(li) 60 | previousLi = li 61 | }) 62 | 63 | return modifiedLis 64 | } 65 | 66 | 67 | const paginationListItem = (page: number, label: string, options: DataTableConfiguration, state: {active?: boolean, hidden?: boolean} = {}) : elementNodeType => ({ 68 | nodeName: "LI", 69 | attributes: { 70 | class: 71 | (state.active && !state.hidden) ? 72 | `${options.classes.paginationListItem} ${options.classes.active}` : 73 | state.hidden ? 74 | `${options.classes.paginationListItem} ${options.classes.hidden} ${options.classes.disabled}` : 75 | options.classes.paginationListItem 76 | }, 77 | childNodes: [ 78 | { 79 | nodeName: "BUTTON", 80 | attributes: { 81 | "data-page": String(page), 82 | class: options.classes.paginationListItemLink, 83 | "aria-label": options.labels.pageTitle.replace("{page}", String(page)) 84 | }, 85 | childNodes: [ 86 | { 87 | nodeName: "#text", 88 | data: label 89 | } 90 | ] 91 | } 92 | ] 93 | }) 94 | 95 | export const createVirtualPagerDOM = (onFirstPage: boolean, onLastPage: boolean, currentPage: number, totalPages: number, options) => { 96 | 97 | let pagerListItems : elementNodeType[] = [] 98 | 99 | // first button 100 | if (options.firstLast) { 101 | pagerListItems.push(paginationListItem(1, options.firstText, options)) 102 | } 103 | 104 | // prev button 105 | if (options.nextPrev) { 106 | const prev = onFirstPage ? 1 : currentPage - 1 107 | pagerListItems.push(paginationListItem(prev, options.prevText, options, {hidden: onFirstPage})) 108 | } 109 | 110 | let pages = [...Array(totalPages).keys()].map(index => paginationListItem(index+1, String(index+1), options, {active: (index === (currentPage-1))})) 111 | 112 | if (options.truncatePager) { 113 | // truncate the paginationListItems 114 | pages = truncate( 115 | pages, 116 | currentPage, 117 | totalPages, 118 | options 119 | ) 120 | 121 | } 122 | 123 | // append the paginationListItems 124 | pagerListItems = pagerListItems.concat(pages) 125 | 126 | // next button 127 | if (options.nextPrev) { 128 | const next = onLastPage ? totalPages : currentPage + 1 129 | pagerListItems.push(paginationListItem(next, options.nextText, options, {hidden: onLastPage})) 130 | } 131 | 132 | // last button 133 | if (options.firstLast) { 134 | pagerListItems.push(paginationListItem(totalPages, options.lastText, options)) 135 | } 136 | 137 | const pager : elementNodeType = { 138 | nodeName: "UL", 139 | attributes: { 140 | class: options.classes.paginationList 141 | }, 142 | childNodes: pages.length > 1 ? pagerListItems : [] // Don't show single page 143 | } 144 | 145 | return pager 146 | 147 | } 148 | -------------------------------------------------------------------------------- /test/cases/cell-attributes-dom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | cell-attributes-dom 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
    NameExt.CityStart DateCompletion
    Unity Pugh9958Curicó2005/02/1137%
    Theodore Duran8971Dhanbad1999/04/0797%
    Kylie Bishop3147Norman2005/09/0863%
    42 | 43 | 44 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/cases/cell-attributes-js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | cell-attributes-js 6 | 7 | 8 |
    9 | 10 | 11 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /test/cases/empty-table-with-footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | empty-table-with-footer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 |
    This is a table caption.
    Header 1Header 2
    22 | This is a table footer. 23 |
    27 | 28 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/cases/multiple-classes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | multiple-classes 6 | 7 | 8 |
    9 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /test/server.mjs: -------------------------------------------------------------------------------- 1 | import process from "process" 2 | import express from "express" 3 | 4 | const app = express() 5 | 6 | app.use(express.static("docs/demos")) 7 | app.use("/tests", express.static("test/cases")) 8 | app.get("/documentation", (_req, res) => res.send("It's me, the documentation page!")) 9 | app.use("/favicon.ico", express.static("docs/favicon.ico")) 10 | app.use("/favicon.svg", express.static("docs/favicon.svg")) 11 | 12 | let serverProcess 13 | 14 | const listen = (port = 3000) => { 15 | serverProcess = app.listen(port, () => console.log(`Server listening on port ${port}.`)) 16 | } 17 | 18 | const close = () => serverProcess.close() && console.log("Closing down server.") 19 | 20 | export const server = {listen, 21 | close} 22 | 23 | if (process.argv[1].includes("server.mjs")) { 24 | server.listen() 25 | } 26 | --------------------------------------------------------------------------------