├── .babelrc ├── .eslintignore ├── .eslintrc ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE └── workflows │ ├── manual-docs-publish.yml │ ├── manual-npm-publish.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .lintstagedrc ├── .npmrc ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── css │ └── custom.css ├── docs │ ├── building-and-testing.md │ ├── examples.md │ ├── examples │ │ ├── 01-create-new-grid.mdx │ │ ├── 02-set-data-after-instantiation.mdx │ │ ├── 03-format-data.mdx │ │ ├── 04-format-data-using-rendertext-event.mdx │ │ ├── 05-use-a-textarea-to-edit-cells-instead-of-an-input.mdx │ │ ├── 06-use-select-instead-of-input-for-edits.mdx │ │ ├── 07-detect-clicks.mdx │ │ ├── 08-detect-cell-over-out.mdx │ │ ├── 09-get-data-via-xhr-function.mdx │ │ ├── 10-set-filter-function.mdx │ │ ├── 11-simple-context-menu.mdx │ │ ├── 12-hierarchal-context-menus.mdx │ │ ├── 13-context-menu-using-a-function-for-items.mdx │ │ ├── 14-asynchronous-context-items.mdx │ │ ├── 15-remove-context-menu-items.mdx │ │ ├── 16-create-complex-context-menu.mdx │ │ ├── 17-alter-runtime-style.mdx │ │ ├── 18-canvas-fill-styles.mdx │ │ ├── 19-alter-startup-styles.mdx │ │ ├── 20-replace-all-styles-at-runtime.mdx │ │ ├── 21-selection-mode-row.mdx │ │ ├── 22-scroll-to-a-cell.mdx │ │ ├── 23-conditionally-set-colors.mdx │ │ ├── 24-validate-input.mdx │ │ ├── 25-edit-a-cell.mdx │ │ ├── 26-set-the-width-of-a-column.mdx │ │ ├── 27-order-by-a-column.mdx │ │ ├── 28-select-an-area.mdx │ │ ├── 29-remove-all-rows.mdx │ │ ├── 30-allow-new-rows.mdx │ │ ├── 31-create-a-web-component-grid.mdx │ │ ├── 32-change-a-columns-title.mdx │ │ ├── 33-add-a-column.mdx │ │ ├── 34-add-a-row.mdx │ │ ├── 35-draw-html-via-event.mdx │ │ ├── 36-draw-html-via-data-type.mdx │ │ ├── 37-open-a-tree.mdx │ │ ├── 39-allow-users-to-open-trees.mdx │ │ ├── 39-multiple-filters.mdx │ │ ├── 40-draw-a-picture.mdx │ │ ├── 41-toggle-debug-data.mdx │ │ ├── 42-display-unicode-samples.mdx │ │ ├── 43-create-a-spreadsheet.mdx │ │ ├── 44-add-10000-random-rows.mdx │ │ └── 45-disco-mode.mdx │ ├── getting-started.md │ ├── intro.md │ └── topics │ │ ├── drawing-on-the-canvas.md │ │ ├── extending-the-visual-appearance.md │ │ ├── formatting-using-event-listeners.md │ │ ├── setting-a-schema.md │ │ ├── setting-and-getting-data.md │ │ ├── setting-height-and-width.md │ │ └── ways-to-create-a-grid.md ├── docusaurus.config.js ├── jsdoc-to-md.js ├── package-lock.json ├── package.json ├── sidebars.js ├── src │ ├── components │ │ ├── HomepageFeatures.js │ │ ├── HomepageFeatures.module.css │ │ └── SandpackEditor │ │ │ ├── createFileMap.ts │ │ │ └── index.tsx │ ├── pages │ │ ├── index.js │ │ ├── index.module.css │ │ └── markdown-page.md │ └── theme │ │ └── MDXComponents.js ├── static │ ├── .nojekyll │ └── img │ │ ├── customizable.svg │ │ ├── datagrid1.png │ │ ├── favicon.ico │ │ ├── high-performance.svg │ │ └── interactive.svg └── templates │ ├── classes.hbs │ ├── events.hbs │ ├── methods.hbs │ ├── parameters.hbs │ ├── properties.hbs │ └── styling.hbs ├── images └── datagrid1.png ├── jsdoc.conf.js ├── jsdoc.ts.conf.js ├── karma.conf.js ├── lib ├── button.js ├── component.js ├── contextMenu.js ├── defaults.js ├── docs.js ├── dom.js ├── draw.js ├── events │ ├── index.js │ └── util.js ├── groups │ └── util.js ├── intf.js ├── main.js ├── publicMethods.js ├── selections │ ├── index.js │ ├── type.d.ts │ └── util.js └── touch.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── test ├── attributes.js ├── context-menu.js ├── data-interface.js ├── drawing.js ├── editing.js ├── filters.js ├── formatters.js ├── groups.js ├── index.html ├── index.js ├── instantation.js ├── key-navigation.js ├── public-interface.js ├── reorder-columns.js ├── resize.js ├── scrolling.js ├── selections.js ├── sorters.js ├── style.js ├── touch.js ├── unhide-indicator.js ├── unit │ ├── index.js │ ├── merge-hidden-row-ranges.js │ ├── parse-clip-board-data.js │ └── selections.js ├── util.js └── web-component.js ├── tutorials ├── amdDemo.html ├── amdDemo.js ├── canvasDatagrid.contextMenuItem.md ├── canvasDatagrid.md ├── canvasDatagrid.style.md ├── css │ └── main.css ├── demo.html ├── demo.js ├── developer.html ├── developer.js ├── index.html ├── js │ └── main.js ├── largeArraysDemo.html ├── largeArraysDemo.js ├── pivotFormDemo.html ├── pivotFormDemo.js ├── publish.js ├── reactExample.html ├── reactExample.js ├── sparklineDemo.html ├── sparklineDemo.js ├── styleBuilder.html ├── styleBuilder.js ├── styleLibrary.js ├── tutorials.js ├── vueExample.html ├── vueExample.js ├── webcomponentDemo.html ├── xhrPagingDemo.html └── xhrPagingDemo.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | docs 4 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "prettier" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": 2018, 9 | "sourceType": "module" 10 | }, 11 | "plugins": [ 12 | "prettier" 13 | ], 14 | "env": { 15 | "browser": true, 16 | "es6": true, 17 | "mocha": true 18 | }, 19 | "globals": { 20 | "chai": true, 21 | "assert": true, 22 | "canvasDatagrid": true 23 | }, 24 | "rules": { 25 | "prettier/prettier": 2, // 2 -> error 26 | "indent": [ 27 | "warn", 28 | 2, 29 | { 30 | "flatTernaryExpressions": true, 31 | "offsetTernaryExpressions": true, 32 | "SwitchCase": 1 33 | } 34 | ], 35 | // These should be turned on after a clean-up: 36 | "no-unused-vars": 0, 37 | "no-redeclare": 0 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Expectation of Behavior 4 | 5 | * Treat everyone with dignity and respect. 6 | * No harassment. 7 | * Gracefully accept criticism. 8 | * No sex. 9 | * No politics. 10 | * No trolling. 11 | 12 | Report unacceptable behavior to tonygermaneri@gmail.com. 13 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Responsibility of Contributors 2 | 3 | - Pull requests must conform to [JSLINT](http://jslint.com/) [ES5](https://es5.github.io/) and [JSDOC](http://usejsdoc.org/). 4 | - Declared exceptions to JSLINT are ok. 5 | - Pull requests must be [attached to an issue](https://github.com/TonyGermaneri/canvas-datagrid/issues). 6 | - Pull requests that effect code must pass [tests](https://canvas-datagrid.js.org/canvas-datagrid/test/tests.html). 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior and actual behavior. 2 | 3 | 4 | ### Steps to reproduce the problem. 5 | 6 | 7 | ### Specifications like the version of the project, operating system, or hardware. 8 | 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | Fixes issue #. 2 | 3 | Changes proposed in this pull request: 4 | 5 | - 6 | - 7 | - 8 | -------------------------------------------------------------------------------- /.github/workflows/manual-docs-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish documentation 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publish-documentation: 7 | name: Publish documentation 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [16.x] 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Install Node 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: '16.x' 21 | - name: Install dependencies 22 | run: npm ci 23 | - name: Build debug version 24 | run: npm run build 25 | - name: Build documentation 26 | run: | 27 | cd docs 28 | npm ci 29 | npm run build 30 | # Required for hosting at js.org 31 | - name: Ensure CNAME is part of gh-pages 32 | run: echo "canvas-datagrid.js.org" > docs/build/CNAME 33 | # This will make documentation available at 34 | # https://.github.io/canvas-datagrid/ 35 | - name: Publish documentation to GitHub pages 36 | uses: peaceiris/actions-gh-pages@v3.7.0-8 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | exclude_assets: '.gitignore,.github,node_modules' 40 | publish_dir: ./docs/build 41 | -------------------------------------------------------------------------------- /.github/workflows/manual-npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Manually publish to NPM 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publish-to-npm: 7 | name: Publish to NPM 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [14.x] 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | registry-url: 'https://registry.npmjs.org/' 22 | - name: Install dependencies 23 | run: npm ci 24 | - name: Build 25 | run: npm run build 26 | - name: Publish NPM Module 27 | uses: JS-DevTools/npm-publish@v2 28 | with: 29 | check-version: false 30 | token: ${{ secrets.NPM_DEPLOY_KEY }} 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release new tagged version 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | jobs: 10 | build: 11 | name: Build and test 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [14.x] 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install dependencies 26 | run: npm ci 27 | - name: Build 28 | run: npm run build 29 | - name: Test 30 | run: npm test 31 | - name: Upload build artifacts 32 | uses: actions/upload-artifact@v2 33 | with: 34 | name: dist 35 | path: ./dist/* 36 | 37 | release: 38 | name: Release 39 | runs-on: ubuntu-latest 40 | needs: build 41 | 42 | strategy: 43 | matrix: 44 | node-version: [14.x] 45 | 46 | steps: 47 | - name: Checkout code 48 | uses: actions/checkout@v2 49 | - name: Download build 50 | uses: actions/download-artifact@v2 51 | with: 52 | name: dist 53 | path: ./dist 54 | - name: Include dist/ folder (the build) in release 55 | run: | 56 | zip -r ${{ github.event.repository.name }}.zip * 57 | - name: Create release 58 | id: create_release 59 | uses: actions/create-release@v1 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | with: 63 | tag_name: ${{ github.ref }} 64 | release_name: Release ${{ github.ref }} 65 | draft: false 66 | prerelease: false 67 | - name: Upload Release Asset 68 | id: upload-release-asset 69 | uses: actions/upload-release-asset@v1 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | with: 73 | # This pulls from the CREATE RELEASE step above, referencing its ID 74 | # to get its outputs object, which include a `upload_url`. See this 75 | # blog post for more info: 76 | # https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 77 | upload_url: ${{ steps.create_release.outputs.upload_url }} 78 | asset_path: ./${{ github.event.repository.name }}.zip 79 | asset_name: ${{ github.event.repository.name }}.zip 80 | asset_content_type: application/zip 81 | - name: Publish NPM Module 82 | uses: JS-DevTools/npm-publish@v2 83 | with: 84 | token: ${{ secrets.NPM_DEPLOY_KEY }} 85 | 86 | publish-documentation: 87 | name: Publish documentation 88 | runs-on: ubuntu-latest 89 | 90 | strategy: 91 | matrix: 92 | node-version: [16.x] 93 | 94 | steps: 95 | - name: Checkout 96 | uses: actions/checkout@v2 97 | - name: Install Node 98 | uses: actions/setup-node@v1 99 | with: 100 | node-version: '16.x' 101 | - name: Install dependencies 102 | run: npm ci 103 | - name: Build debug version 104 | run: npm run build 105 | - name: Build documentation 106 | run: | 107 | cd docs 108 | npm ci 109 | npm run build 110 | # Required for hosting at js.org 111 | - name: Ensure CNAME is part of gh-pages 112 | run: echo "canvas-datagrid.js.org" > docs/build/CNAME 113 | # This will make documentation available at 114 | # https://.github.io/canvas-datagrid/ 115 | - name: Publish documentation to GitHub pages 116 | uses: peaceiris/actions-gh-pages@v3.7.0-8 117 | with: 118 | github_token: ${{ secrets.GITHUB_TOKEN }} 119 | exclude_assets: '.gitignore,.github,node_modules' 120 | publish_dir: ./docs/build -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Test canvas-datagrid 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x] 19 | 20 | steps: 21 | - name: Checkout code from repository 22 | uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: Install dependencies 28 | run: npm ci 29 | - name: Lint 30 | run: npm run lint 31 | - name: Test 32 | run: npm test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | build 5 | dist 6 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "lib/**/*.js": ["prettier --write", "eslint --fix"], 3 | "*.+(json|css)": ["prettier --write"] 4 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing to canvas-datagrid 3 | 4 | ## Introduction 5 | 6 | First of all, thank you so much for taking the time to contribute! 🎉👍🎈 The maintainers of this project have day jobs and other commitments, so our time is limited and we really welcome any help we can get! 7 | 8 | ![cat-cute](https://user-images.githubusercontent.com/950979/141787558-3ffe531d-7eaf-4615-b8ed-7eaf58dce067.gif) 9 | 10 | This document describes some guidelines and best practices for contributing to canvas-datagrid. Before submitting a pull request, please make sure you've read this entire document, as it contains some helpful pointers and aims to save time and effort for both maintainers and contributors alike. 11 | 12 | ## Conventions 13 | 14 | ### Commit messages 15 | 16 | Clear, concise commit messages make all the difference when submitting pull requests. When done properly, it allows maintainers to quickly understand the different components or aspects of the changes introduced by a pull request. Therefore, when writing commit messages, please adhere to the following guidelines: 17 | 18 | - Use the present tense, so "Add feature" not "Added feature" 19 | - Use the imperative: "Move cursor to..." not "Moves cursor to..." 20 | - Limit the first line to 72 characters or less 21 | - Larger commits may have large messages, but keep the first line short and expand in more detail in the following lines 22 | - Reference issues and pull requests liberally after the first line 23 | 24 | If you keep these in mind while writing your code, it becomes more natural to commit smaller and more coherent sets of changes instead of big-ball-of-mud commits, and it tends to help you structure the work into discrete steps. This one is hard to get right, and even experienced developers often end up mashing a bunch of unrelated changes together in a single commit (🙋🏻‍♂️), so don't feel bad if you have to commit several things at once, but try to keep it in mind. 25 | 26 | ### Pull requests 27 | 28 | A good pull request makes it easy for a maintainer to review. This means the pull request clearly and succinctly describes the changes being introduced, provides a commit history that makes it easy to evaluate distinct sets of changes 29 | 30 | #### Structuring commits in a pull requests 31 | 32 | The best pull requests are made up of commits that are structured in such a way that each commit is a logical, coherent change. If you're submitting a bugfix, the pull request should only address one single issue. Do not group multiple bugs or issues into the same pull request. If you do group several issues in one pull request, it makes it exponentially harder for a maintainer to evaluate whether the proposed changes address the relevant issue(s). There are exceptions to this rule, but try to keep pull requests as lean and to the point as possible. 33 | 34 | 35 | #### Tests 36 | 37 | Pull requests with failing tests or without tests that cover the new or changed functionality will _not_ be reviewed. This sounds tough, but while we welcome your contribution, without proper tests it's very hard and/or time-consuming for maintainers to evaluate whether the changes do what they should. Often a simple test case or two is enough. 38 | 39 | Run tests using: 40 | 41 | ``` 42 | npm test 43 | ``` 44 | 45 | Tests are located in `test/`. If you need help creating tests, please reach out to us on Slack. 46 | 47 | #### Create an online example 48 | 49 | If you can, setting up a demo of the changes in an online code editor like [repl.it](https://replit.com/) or [CodeSandbox](https://codesandbox.io/) would make all the difference. It then becomes a lot easier for maintainers to quickly check if everything's working as described, and your pull request will get reviewed and approved a lot faster. It's not a hard requirement, but it definitely helps to speed things along. 50 | 51 | ### Javascript code 52 | 53 | All source code should be formatted using [prettier](https://prettier.io/) and checked using [eslint](https://eslint.org/). To format, run: 54 | 55 | ``` 56 | npm run format 57 | ``` 58 | 59 | Please ensure before submitting the pull request that eslint does not report any errors. To do this, run: 60 | 61 | ``` 62 | npm run lint 63 | ``` 64 | 65 | While not perfect, these two commands will catch most of the common issues. Any other issues are described below. Note: this is a work in progress, so please don't feel bad if we make note of something not mentioned here. 66 | 67 | #### General background 68 | 69 | The codebase for this project has been around for a number of years and is in need of some refactoring and cleaning up. While now the code is annotated using JSDoc to describe types, we would like to migrate this project to TypeScript and make use of its native type system for an improved developer experience and keeping documentation in sync. This is all to say, parts of the codebase need some work, so when making changes try to see if you can raise the general level of quality of the codebase. 70 | 71 | #### Comments 72 | 73 | - Do not leave commented out code. Either remove it, or refactor it. Old code hanging around in comments is nobody's idea of a good time, serves no purpose, and should be removed. 74 | - Comments should describe _why_ not _how_. The _how_ should be clear by the code itself. If it's not, the code needs to be reworked until it can be made clear. Good comments describe _why_ a certain piece of code is present. 75 | 76 | #### Assorted 77 | 78 | - Please use descriptive variable names as much as possible 79 | - Please use `const` and `let` over `var` (in that order) 80 | 81 | ## Thank you! 82 | 83 | Thank you for reading — we appreciate it! 84 | 85 | ![FD3h0wFVIAAGDmj](https://user-images.githubusercontent.com/950979/141791902-e71d67b8-3359-4503-aaf8-ca48255b48f7.jpg) 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Tony Germaneri 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name of lice nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Instructions for maintainers 2 | 3 | ## Releasing a new version 4 | 5 | To release a new version of canvas-datagrid, I've found that this order of steps works well: 6 | 7 | 1. Update the `CHANGELOG.md` file, with the new version number, date, and an overview of what's changed since the previous release. For example: 8 | 9 | ``` 10 | ## 1.0.0 - 2025-06-01 11 | 12 | ### Added 13 | 14 | - Big feature meriting 1.0.0 release (contributing-user-foo, #999) 15 | ``` 16 | 17 | 2. Commit this file 18 | 19 | 3. Run `npm version patch` or `npm version `, but usually `patch` will suffice. This will update the package.json and package-lock.json files, commit them, and tag the commit with the new version number. 20 | 21 | 4. Push the commits and tags 22 | 23 | ``` 24 | git push && git push --tags 25 | ``` 26 | 27 | 5. Once you've pushed the tagged commit, the GitHub Actions CI pipeline will take care of releasing it to NPM. To see how that works, refer to `.github/workflows/release.yml```` 28 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | docs/reference 11 | 12 | # Misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # On documentation 2 | 3 | This directory contains the documentation for canvas-datagrid. It's a combination of static Markdown files 4 | located in `docs/` and Markdown files generated by `jsdoc-to-md.js`, based on the JSDoc annotations in the 5 | the source code (specifically, `lib/docs.js` and `lib/publicMethods.js`). 6 | 7 | The documentation build system is [Docusaurus](https://docusaurus.io/). For more details on how it all works, 8 | please visit the documentation for Docusaurus. While the documentation system is built and published 9 | automatically when a new release is made, it helps to know how to do it locally, for when modifying or adding 10 | pages. 11 | 12 | In `docs/` 13 | 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | This will first run `jsdoc-to-md.js`, which parses all the JSDoc annotations in `lib/docs.js` and `lib/publicMethods.js` 19 | and then generates Markdown files in `docs/reference`. These files are generated using [Handlebars](https://handlebarsjs.com/) templates located in `templates/` 20 | 21 | After that, Docusaurus will build these and the remainder of the pages,compiling and packaging everything into a React app. 22 | 23 | More convenient when authoring documents, though, is running 24 | 25 | ``` 26 | npm run docusaurus 27 | ``` 28 | 29 | This will run the Docusaurus development server, compiling and refreshing documents as you modify them. 30 | 31 | ## Adding document to sidebar 32 | 33 | The sidebar (left) will contain links to the documents in `docs/`. This table of contents is generated in the file `sidebars.js`. When adding a document, and you don't see it show up in the sidebar, you'll probably have to add it manually in `sidebars.js` 34 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #354b61; 10 | --ifm-color-primary-dark: rgb(33, 175, 144); 11 | --ifm-color-primary-darker: rgb(31, 165, 136); 12 | --ifm-color-primary-darkest: rgb(26, 136, 112); 13 | --ifm-color-primary-light: rgb(70, 203, 174); 14 | --ifm-color-primary-lighter: rgb(102, 212, 189); 15 | --ifm-color-primary-lightest: rgb(146, 224, 208); 16 | --ifm-code-font-size: 95%; 17 | --ifm-link-color: #2980b9; 18 | --ifm-navbar-link-color: #f5f5f5; 19 | } 20 | 21 | .docusaurus-highlight-code-line { 22 | background-color: rgba(0, 0, 0, 0.1); 23 | display: block; 24 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 25 | padding: 0 var(--ifm-pre-padding); 26 | } 27 | 28 | html[data-theme='light'] body { 29 | background-color: #ecf0f1; 30 | } 31 | 32 | html[data-theme='dark'] .docusaurus-highlight-code-line { 33 | background-color: rgba(0, 0, 0, 0.3); 34 | } 35 | 36 | html[data-theme='light'] .navbar { 37 | background-color: #2c3e50; 38 | } 39 | 40 | .properties-object-type { 41 | color: var(--ifm-color-secondary-darkest); 42 | } 43 | 44 | a.table-of-contents__link.toc-highlight .properties-object-type { 45 | display: none; 46 | } 47 | -------------------------------------------------------------------------------- /docs/docs/building-and-testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Building and Testing 3 | --- 4 | 5 | To install development dependencies (required to build or test): 6 | 7 | ``` 8 | npm install 9 | ``` 10 | 11 | To build production and debug versions: 12 | 13 | ``` 14 | npm run build 15 | ``` 16 | 17 | To build documentation: 18 | 19 | ``` 20 | npm run build:docs 21 | ``` 22 | 23 | To convert the JSDoc annotation to type definition files for TypeScript integration: 24 | 25 | ``` 26 | npm run build:types 27 | ``` 28 | 29 | To run tests. Note: Headless tests will mostly fail due to lack of headless canvas pixel detection support. Use VM testing or your browser. 30 | 31 | ``` 32 | npm test 33 | ``` 34 | 35 | ### Windows 10 WSL Testing 36 | *This is info for wsl version 1. v2 seems to be [different](https://dev.to/davelsan/comment/nnf5).* 37 | 38 | - `CHROME_BIN` needs to be set to the location of your Google Chrome exe in Windows. (e.g. `/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe`) 39 | *in WSL, `export CHROME_BIN='path/to/chrome'`* 40 | - Chrome needs access to [karma's temp folder](https://stackoverflow.com/a/56204265/292067). 41 | - Create a `tmp` folder on the same Windows drive as your repo. 42 | - set `TEMP` to a folder that exists on the same Windows drive as your repo. (matching capitalization probably matters) 43 | *in WSL, `export TEMP='/Temp/karma'`, if your repo is on drive C, then create folder C:\Temp\karma* 44 | - `karma.conf.js` needs to be edited 45 | - Change the browser from `ChromeHeadless` to `Chrome` 46 | - Modify to run ChromeHeadless without sandboxing. This is not ideal, but it seems to be necessary in [WSL](https://github.com/microsoft/WSL/issues/3282) and [Linux containers](https://docs.travis-ci.com/user/chrome#sandboxing) ([see also](https://github.com/karma-runner/karma-chrome-launcher/issues/158#issuecomment-339265457)) 47 | - Add a custom launcher 48 | ``` 49 | customLaunchers: { 50 | ChromeHeadlessNoSandbox: { 51 | base: 'ChromeHeadless', 52 | flags: ['--no-sandbox'] 53 | } 54 | } 55 | ``` 56 | - Change the browser from `ChromeHeadless` to `ChromeHeadlessNoSandbox` 57 | -------------------------------------------------------------------------------- /docs/docs/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction examples 3 | --- 4 | 5 | # Introduction examples 6 | 7 | A significant number of code samples have been added here. You are invited to experiment and play around with the code in the interactive code-sandboxes to gain a better understanding of how the library works and how it can be used in different scenarios. 8 | -------------------------------------------------------------------------------- /docs/docs/examples/01-create-new-grid.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create a new grid 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | A grid is created and data is set in one command. Data can be an array of objects, an array of arrays or a mixed array of objects, arrays and primitives.' 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | ``` 24 | 25 | ```json data.json 26 | [ 27 | { "col1": "foo", "col2": 0, "col3": "a" }, 28 | { "col1": "bar", "col2": 1, "col3": "b" }, 29 | { "col1": "baz", "col2": 2, "col3": "c" } 30 | ] 31 | ``` 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/docs/examples/02-set-data-after-instantiation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Set data after instantiation 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | A grid is created, then data is set afterwards. You can set data using `grid.data = data;` at anytime. Data can be an array of objects, an array of arrays or a mixed array of objects, arrays and primitives. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | }); 20 | 21 | app.append(gridElement); 22 | 23 | grid.data = data; 24 | ``` 25 | 26 | ```json data.json 27 | [ 28 | { "col1": "foo", "col2": 0, "col3": "a" }, 29 | { "col1": "bar", "col2": 1, "col3": "b" }, 30 | { "col1": "baz", "col2": 2, "col3": "c" } 31 | ] 32 | ``` 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/docs/examples/03-format-data.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Format data 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Data is formatted using the data type setting in the schema. The data type is mapped to the `grid.formatters` object. In the following example col2 is data type `date` which will format data using the `grid.formatters.date` function. By default the string formatter is used to format all data. This method of formatting is faster than using the [`rendertext`](/reference/events#rendertext) event. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | schema: [ 21 | { name: 'col1' }, 22 | { 23 | name: 'col2', 24 | type: 'date', 25 | }, 26 | { name: 'col3' }, 27 | ], 28 | }); 29 | 30 | grid.formatters.date = function (e) { 31 | return new Date(e.cell.value).toISOString(); 32 | }; 33 | 34 | app.append(gridElement); 35 | ``` 36 | 37 | ```json data.json 38 | [ 39 | { "col1": "foo", "col2": 1501744914661, "col3": "a" }, 40 | { "col1": "bar", "col2": 1301744914661, "col3": "b" }, 41 | { "col1": "baz", "col2": 1401744914661, "col3": "c" } 42 | ] 43 | ``` 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/docs/examples/04-format-data-using-rendertext-event.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Format data using `rendertext` event 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | NB: This example is broken: https://github.com/TonyGermaneri/canvas-datagrid/issues/262 8 | 9 | Attach to the [`rendertext`](/reference/events#rendertext) event. 10 | 11 | 12 | 13 | ```ts 14 | import canvasDatagrid from 'canvas-datagrid'; 15 | import data from '/data.json'; 16 | 17 | const app = document.getElementById('app'); 18 | const gridElement = document.createElement('div'); 19 | const grid = canvasDatagrid({ 20 | parentNode: gridElement, 21 | data, 22 | schema: [ 23 | { name: 'col1' }, 24 | { 25 | name: 'col2', 26 | type: 'date', 27 | }, 28 | { name: 'col3' }, 29 | ], 30 | }); 31 | 32 | grid.addEventListener('rendertext', function (e) { 33 | if (e.cell.rowIndex > -1) { 34 | if (e.cell.header.name === 'col2') { 35 | e.cell.formattedValue = new Date(e.cell.value).toISOString(); 36 | } 37 | } 38 | }); 39 | 40 | app.append(gridElement); 41 | ``` 42 | 43 | ```json data.json 44 | [ 45 | { "col1": "foo", "col2": 1501744914661, "col3": "a" }, 46 | { "col1": "bar", "col2": 1301744914661, "col3": "b" }, 47 | { "col1": "baz", "col2": 1401744914661, "col3": "c" } 48 | ] 49 | ``` 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/docs/examples/05-use-a-textarea-to-edit-cells-instead-of-an-input.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Use a textarea to edit cells instead of an input. 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Use a textarea to edit cells instead of an input 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | multiLine: true, 21 | }); 22 | 23 | app.append(gridElement); 24 | 25 | grid.style.cellHeight = 80; 26 | ``` 27 | 28 | ```json data.json 29 | [ 30 | { "col1": "foo\nbar", "col2": 0, "col3": "a" }, 31 | { "col1": "bar\nfoo\nbar", "col2": 1, "col3": "b" }, 32 | { "col1": "baz\nfoo\nbar", "col2": 2, "col3": "c" } 33 | ] 34 | ``` 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/docs/examples/06-use-select-instead-of-input-for-edits.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Use select instead of input for edits 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | When a column in the schema includes an `enum` property, a drop down menu will appear instead of the normal input or textarea. 8 | 9 | 10 | 11 | ```ts 12 | import data from '/data.json'; 13 | import canvasDatagrid from 'canvas-datagrid'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | schema: [ 21 | { 22 | name: 'col1', 23 | enum: [ 24 | ['foo', 'Foo'], 25 | ['bar', 'Bar'], 26 | ['baz', 'Baz'], 27 | ], 28 | }, 29 | { 30 | name: 'col2', 31 | }, 32 | { 33 | name: 'col3', 34 | }, 35 | ], 36 | }); 37 | 38 | app.append(gridElement); 39 | ``` 40 | 41 | ```json data.json 42 | [ 43 | { "col1": "foo", "col2": 0, "col3": "a" }, 44 | { "col1": "bar", "col2": 1, "col3": "b" }, 45 | { "col1": "baz", "col2": 2, "col3": "c" } 46 | ] 47 | ``` 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/docs/examples/07-detect-clicks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Detect clicks 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Detect which cell was clicked using the `click` event. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | grid.addEventListener('click', function (e) { 23 | if (!e.cell) return; 24 | grid.data[0][grid.schema[0].name] = 'Clicked on ' + e.cell.value; 25 | }; 26 | 27 | app.append(gridElement); 28 | ``` 29 | 30 | ```json data.json 31 | [ 32 | { "col1": "foo\nbar", "col2": 0, "col3": "a" }, 33 | { "col1": "bar\nfoo\nbar", "col2": 1, "col3": "b" }, 34 | { "col1": "baz\nfoo\nbar", "col2": 2, "col3": "c" } 35 | ] 36 | ``` 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/docs/examples/08-detect-cell-over-out.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Detect cell over/out 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Detect when a cell has been entered using `cellmouseover` and `cellmouseout` events. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | grid.addEventListener('cellmouseover', function (e) { 23 | if (!e.cell) { 24 | return; 25 | } 26 | grid.data[0][grid.schema[0].name] = 27 | 'Just arrived at ' + e.cell.columnIndex + ', ' + e.cell.rowIndex; 28 | }); 29 | grid.addEventListener('cellmouseout', function (e) { 30 | if (!e.cell) { 31 | return; 32 | } 33 | grid.data[1][grid.schema[0].name] = 34 | 'Just came from ' + e.cell.columnIndex + ', ' + e.cell.rowIndex; 35 | }); 36 | 37 | app.append(gridElement); 38 | ``` 39 | 40 | ```json data.json 41 | [ 42 | { "col1": "foo\nbar", "col2": 0, "col3": "a" }, 43 | { "col1": "bar\nfoo\nbar", "col2": 1, "col3": "b" }, 44 | { "col1": "baz\nfoo\nbar", "col2": 2, "col3": "c" } 45 | ] 46 | ``` 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/docs/examples/09-get-data-via-xhr-function.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Get data via XHR function 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Fetch data from data.gov and parse the JSON. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import parseOpenData from '/open-data'; 14 | 15 | const xhr = new XMLHttpRequest(); 16 | const app = document.getElementById('app'); 17 | const grid = canvasDatagrid({ 18 | parentNode: app, 19 | }); 20 | 21 | grid.style.height = '300px'; 22 | grid.style.width = '100%'; 23 | 24 | xhr.addEventListener('progress', function (event) { 25 | grid.data = [ 26 | { 27 | status: 28 | 'Loading data: ' + 29 | event.loaded + 30 | ' of ' + 31 | (event.total || 'unknown') + 32 | ' bytes...', 33 | }, 34 | ]; 35 | }); 36 | 37 | xhr.addEventListener('load', function (event) { 38 | grid.data = [{ status: 'Loading data ' + event.loaded + '...' }]; 39 | const openData = parseOpenData(JSON.parse(this.responseText)); 40 | 41 | grid.schema = openData.schema; 42 | grid.data = openData.data; 43 | }); 44 | 45 | xhr.open( 46 | 'GET', 47 | 'https://data.cityofchicago.org/api/views/xzkq-xp2w/rows.json?accessType=DOWNLOAD', 48 | ); 49 | 50 | xhr.send(); 51 | ``` 52 | 53 | ```ts open-data.ts 54 | export default function (openData) { 55 | const schema = openData.meta.view.columns; 56 | const data = openData.data.map(function (row) { 57 | var r = {}; 58 | 59 | schema.forEach(function (column, index) { 60 | r[column.name] = row[index]; 61 | }); 62 | 63 | return r; 64 | }); 65 | 66 | return { 67 | data: data, 68 | schema: schema, 69 | }; 70 | } 71 | ``` 72 | 73 | 74 | -------------------------------------------------------------------------------- /docs/docs/examples/10-set-filter-function.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Set filter function 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | By default, the filter is a RegExp string, you can alter this per data type by adding a function to the object grid.filters.<type>. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | schema: [ 21 | { name: 'id', type: 'number' }, 22 | { name: 'offendit', type: 'string' }, 23 | ], 24 | }); 25 | 26 | app.append(gridElement); 27 | 28 | grid.filters.number = function (value, filterFor) { 29 | if (!filterFor) { 30 | return true; 31 | } 32 | 33 | return value === filterFor; 34 | }; 35 | grid.setFilter('id', 1); 36 | ``` 37 | 38 | ```json data.json 39 | [ 40 | { "id": 0, "offendit": "foo" }, 41 | { "id": 1, "offendit": "bar" }, 42 | { "id": 2, "offendit": "baz" } 43 | ] 44 | ``` 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/docs/examples/11-simple-context-menu.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Simple context menu 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | NB: This example seems to be broken, calling `selectArea` is not selecting cells. 8 | 9 | Add your own items to the context menu. 10 | 11 | 12 | 13 | ```ts 14 | import canvasDatagrid from 'canvas-datagrid'; 15 | import data from '/data.json'; 16 | 17 | const app = document.getElementById('app'); 18 | const gridElement = document.createElement('div'); 19 | const grid = canvasDatagrid({ 20 | parentNode: gridElement, 21 | data, 22 | }); 23 | 24 | grid.addEventListener('contextmenu', function (e) { 25 | e.items.push({ 26 | title: 'Select all', 27 | click: function (ev) { 28 | grid.selectArea({ 29 | top: 0, 30 | bottom: 2, 31 | left: 0, 32 | right: 2, 33 | }); 34 | grid.draw(); 35 | }, 36 | }); 37 | }); 38 | 39 | app.append(gridElement); 40 | ``` 41 | 42 | ```json data.json 43 | [ 44 | { "col1": "foo", "col2": 0, "col3": "a" }, 45 | { "col1": "bar", "col2": 1, "col3": "b" }, 46 | { "col1": "baz", "col2": 2, "col3": "c" } 47 | ] 48 | ``` 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/docs/examples/12-hierarchal-context-menus.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hierarchal context menus 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Add hierarchal items to the context menu. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | grid.addEventListener('contextmenu', function (e) { 23 | e.items.push({ 24 | title: 'Top level item', 25 | items: [ 26 | { 27 | title: 'Child item #1', 28 | click: function (ev) { 29 | grid.data[0].col1 = e.cell.value; 30 | grid.draw(); 31 | }, 32 | }, 33 | { 34 | title: 'Child item #2', 35 | click: function (ev) { 36 | grid.data[0].col1 = e.cell.value; 37 | grid.draw(); 38 | }, 39 | }, 40 | ], 41 | }); 42 | e.items.push({ 43 | title: 44 | 'You have ' + 45 | grid.selectedRows.filter(function (row) { 46 | return !!row; 47 | }).length + 48 | ' rows selected', 49 | }); 50 | }); 51 | 52 | app.append(gridElement); 53 | ``` 54 | 55 | ```json data.json 56 | [ 57 | { "col1": "foo", "col2": 0, "col3": "a" }, 58 | { "col1": "bar", "col2": 1, "col3": "b" }, 59 | { "col1": "baz", "col2": 2, "col3": "c" } 60 | ] 61 | ``` 62 | 63 | 64 | -------------------------------------------------------------------------------- /docs/docs/examples/13-context-menu-using-a-function-for-items.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Context menu using a function for items 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Instead of using an array, you can use a function that returns an array. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | grid.addEventListener('contextmenu', function (e) { 23 | e.items.push({ 24 | title: 'Function based child context menu item', 25 | items: function () { 26 | return [ 27 | { 28 | title: 'I was added via a function', 29 | click: function () { 30 | return; 31 | }, 32 | }, 33 | ]; 34 | }, 35 | }); 36 | }); 37 | 38 | app.append(gridElement); 39 | ``` 40 | 41 | ```json data.json 42 | [ 43 | { "col1": "foo", "col2": 0, "col3": "a" }, 44 | { "col1": "bar", "col2": 1, "col3": "b" }, 45 | { "col1": "baz", "col2": 2, "col3": "c" } 46 | ] 47 | ``` 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/docs/examples/14-asynchronous-context-items.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Asynchronous context items 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Instead of an array, you can use a function that invokes a callback. When you invoke the callback you pass an array to it to add items to the context menu asynchronously. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | grid.addEventListener('contextmenu', function (e) { 23 | e.items.push({ 24 | title: 'Asynchronous child context menu item', 25 | items: function (callback) { 26 | setTimeout(function () { 27 | callback([ 28 | { 29 | title: 'I was added later', 30 | click: function () { 31 | return; 32 | }, 33 | }, 34 | ]); 35 | }, 500); 36 | }, 37 | }); 38 | }); 39 | 40 | app.append(gridElement); 41 | ``` 42 | 43 | ```json data.json 44 | [ 45 | { "col1": "foo", "col2": 0, "col3": "a" }, 46 | { "col1": "bar", "col2": 1, "col3": "b" }, 47 | { "col1": "baz", "col2": 2, "col3": "c" } 48 | ] 49 | ``` 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/docs/examples/15-remove-context-menu-items.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Remove context menu items 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | You can mutate the existing items in the context menu. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | grid.addEventListener('contextmenu', function (e) { 23 | e.items.splice(0, e.items.length); 24 | e.items.push({ 25 | title: 'Just me now', 26 | }); 27 | }); 28 | 29 | app.append(gridElement); 30 | ``` 31 | 32 | ```json data.json 33 | [ 34 | { "col1": "foo", "col2": 0, "col3": "a" }, 35 | { "col1": "bar", "col2": 1, "col3": "b" }, 36 | { "col1": "baz", "col2": 2, "col3": "c" } 37 | ] 38 | ``` 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/docs/examples/16-create-complex-context-menu.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create complex context menu 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | If you set the value of title to a HTML element reference, you can add complex functionality to the context menu. To prevent the context menu from closing call `e.stopPropagation` on the object clicked on. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const content = document.createElement('div'); 17 | const upButton = document.createElement('button'); 18 | const downButton = document.createElement('button'); 19 | const gridElement = document.createElement('div'); 20 | const grid = canvasDatagrid({ 21 | parentNode: gridElement, 22 | data, 23 | }); 24 | 25 | app.append(gridElement); 26 | 27 | grid.style.height = '250px'; 28 | content.appendChild(upButton); 29 | content.appendChild(downButton); 30 | upButton.innerHTML = 'Scroll Up'; 31 | downButton.innerHTML = 'Scroll Down'; 32 | upButton.addEventListener('click', function (e) { 33 | grid.scrollTop -= 10; 34 | e.stopPropagation(); 35 | }); 36 | downButton.addEventListener('click', function (e) { 37 | grid.scrollTop += 10; 38 | e.stopPropagation(); 39 | }); 40 | content.addEventListener('click', function (e) { 41 | e.stopPropagation(); 42 | }); 43 | grid.addEventListener('contextmenu', function (e) { 44 | e.items.splice(0, e.items.length); 45 | e.items.push({ 46 | title: content, 47 | }); 48 | }); 49 | ``` 50 | 51 | ```json data.json 52 | [ 53 | { "col1": "foo", "col2": 0, "col3": "a" }, 54 | { "col1": "bar", "col2": 1, "col3": "b" }, 55 | { "col1": "baz", "col2": 2, "col3": "c" }, 56 | { "col1": "foo", "col2": 0, "col3": "a" }, 57 | { "col1": "bar", "col2": 1, "col3": "b" }, 58 | { "col1": "baz", "col2": 2, "col3": "c" }, 59 | { "col1": "foo", "col2": 0, "col3": "a" }, 60 | { "col1": "bar", "col2": 1, "col3": "b" }, 61 | { "col1": "baz", "col2": 2, "col3": "c" }, 62 | { "col1": "foo", "col2": 0, "col3": "a" }, 63 | { "col1": "bar", "col2": 1, "col3": "b" }, 64 | { "col1": "baz", "col2": 2, "col3": "c" } 65 | ] 66 | ``` 67 | 68 | 69 | -------------------------------------------------------------------------------- /docs/docs/examples/17-alter-runtime-style.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Alter runtime style 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Change styles after the grid has been instantiated. All styles can be changed at any time. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.style.columnHeaderCellBackgroundColor = 'dodgerblue'; 25 | grid.style.columnHeaderCellColor = 'white'; 26 | ``` 27 | 28 | ```json data.json 29 | [ 30 | { "col1": "foo", "col2": 0, "col3": "a" }, 31 | { "col1": "bar", "col2": 1, "col3": "b" }, 32 | { "col1": "baz", "col2": 2, "col3": "c" } 33 | ] 34 | ``` 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/docs/examples/18-canvas-fill-styles.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Canvas fill styles 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | An example of using complex fill styles on the canvas. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | const gradient = grid.ctx.createLinearGradient(0, 0, 1300, 1300); 22 | 23 | gradient.addColorStop(0, 'dodgerblue'); 24 | gradient.addColorStop(1, 'chartreuse'); 25 | grid.style.cellBackgroundColor = gradient; 26 | grid.style.gridBackgroundColor = gradient; 27 | 28 | app.append(gridElement); 29 | ``` 30 | 31 | ```json data.json 32 | [ 33 | { "col1": "foo", "col2": 0, "col3": "a" }, 34 | { "col1": "bar", "col2": 1, "col3": "b" }, 35 | { "col1": "baz", "col2": 2, "col3": "c" } 36 | ] 37 | ``` 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/docs/examples/19-alter-startup-styles.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Alter startup styles 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Change the styles during instantiation. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | style: { 21 | cellBackgroundColor: 'navy', 22 | cellColor: 'wheat', 23 | }, 24 | }); 25 | 26 | app.append(gridElement); 27 | ``` 28 | 29 | ```json data.json 30 | [ 31 | { "col1": "foo", "col2": 0, "col3": "a" }, 32 | { "col1": "bar", "col2": 1, "col3": "b" }, 33 | { "col1": "baz", "col2": 2, "col3": "c" } 34 | ] 35 | ``` 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/docs/examples/20-replace-all-styles-at-runtime.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Replace all styles at runtime 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Replace the entire style object at runtime. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | grid.style = { 23 | cellBackgroundColor: 'darkred', 24 | cellColor: 'goldenrod', 25 | }; 26 | 27 | app.append(gridElement); 28 | ``` 29 | 30 | ```json data.json 31 | [ 32 | { "col1": "foo", "col2": 0, "col3": "a" }, 33 | { "col1": "bar", "col2": 1, "col3": "b" }, 34 | { "col1": "baz", "col2": 2, "col3": "c" } 35 | ] 36 | ``` 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/docs/examples/21-selection-mode-row.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Selection Mode Row 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Prevents the selection of individual cells forcing the row to become selected. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | selectionMode: 'row', 21 | }); 22 | 23 | app.append(gridElement); 24 | ``` 25 | 26 | ```json data.json 27 | [ 28 | { "col1": "foo", "col2": 0, "col3": "a" }, 29 | { "col1": "bar", "col2": 1, "col3": "b" }, 30 | { "col1": "baz", "col2": 2, "col3": "c" } 31 | ] 32 | ``` 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/docs/examples/22-scroll-to-a-cell.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Scroll to a cell 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Scroll to a cell 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.style.height = '100px'; 25 | grid.scrollIntoView(2, 11); 26 | ``` 27 | 28 | ```json data.json 29 | [ 30 | { "col1": "foo", "col2": 0, "col3": "a" }, 31 | { "col1": "bar", "col2": 1, "col3": "b" }, 32 | { "col1": "baz", "col2": 2, "col3": "c" }, 33 | { "col1": "foo", "col2": 0, "col3": "a" }, 34 | { "col1": "bar", "col2": 1, "col3": "b" }, 35 | { "col1": "baz", "col2": 2, "col3": "c" }, 36 | { "col1": "foo", "col2": 0, "col3": "a" }, 37 | { "col1": "bar", "col2": 1, "col3": "b" }, 38 | { "col1": "baz", "col2": 2, "col3": "c" }, 39 | { "col1": "foo", "col2": 0, "col3": "a" }, 40 | { "col1": "bar", "col2": 1, "col3": "b" }, 41 | { "col1": "baz", "col2": 2, "col3": "c" } 42 | ] 43 | ``` 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/docs/examples/23-conditionally-set-colors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Conditionally set colors 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Change styles during the `rendercell` event. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | grid.addEventListener('rendercell', function (e) { 23 | if (e.cell.header.name === 'Ei' && /omittam/.test(e.cell.value)) { 24 | e.ctx.fillStyle = '#AEEDCF'; 25 | } 26 | }); 27 | 28 | app.append(gridElement); 29 | ``` 30 | 31 | ```json data.json 32 | [ 33 | { "Ei": "principes", "melius": "causae" }, 34 | { "Ei": "omittam", "melius": "audire" }, 35 | { "Ei": "mea", "melius": "quot" }, 36 | { "Ei": "pericula", "melius": "offendit" } 37 | ] 38 | ``` 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/docs/examples/24-validate-input.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Validate input 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | In this example, by using the `beforeendedit` event you can prevent digits from being entered. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | grid.addEventListener('beforeendedit', function (event) { 23 | if (/\d+/.test(event.newValue)) { 24 | alert('NO DIGITS!'); 25 | event.preventDefault(); 26 | } 27 | }); 28 | 29 | app.append(gridElement); 30 | ``` 31 | 32 | ```json data.json 33 | [ 34 | { "col1": "foo", "col2": 0, "col3": "a" }, 35 | { "col1": "bar", "col2": 1, "col3": "b" }, 36 | { "col1": "baz", "col2": 2, "col3": "c" } 37 | ] 38 | ``` 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/docs/examples/25-edit-a-cell.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Edit a cell 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Use the interface to focus a cell, then begin editing a cell.' 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.scrollIntoView(2, 3); 25 | grid.beginEditAt(2, 3); 26 | ``` 27 | 28 | ```json data.json 29 | [ 30 | { "col1": "foo", "col2": 0, "col3": "a" }, 31 | { "col1": "bar", "col2": 1, "col3": "b" }, 32 | { "col1": "baz", "col2": 2, "col3": "c" }, 33 | { "col1": "foo", "col2": 0, "col3": "a" }, 34 | { "col1": "bar", "col2": 1, "col3": "b" }, 35 | { "col1": "baz", "col2": 2, "col3": "c" }, 36 | { "col1": "foo", "col2": 0, "col3": "a" }, 37 | { "col1": "bar", "col2": 1, "col3": "b" }, 38 | { "col1": "baz", "col2": 2, "col3": "c" }, 39 | { "col1": "foo", "col2": 0, "col3": "a" }, 40 | { "col1": "bar", "col2": 1, "col3": "b" }, 41 | { "col1": "baz", "col2": 2, "col3": "c" } 42 | ] 43 | ``` 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/docs/examples/26-set-the-width-of-a-column.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Set the width of a column 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Set the width of a column 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.setColumnWidth(0, 60); 25 | grid.setColumnWidth(1, 200); 26 | ``` 27 | 28 | ```json data.json 29 | [ 30 | { "col1": "foo", "col2": 0, "col3": "a" }, 31 | { "col1": "bar", "col2": 1, "col3": "b" }, 32 | { "col1": "baz", "col2": 2, "col3": "c" } 33 | ] 34 | ``` 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/docs/examples/27-order-by-a-column.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Order by a column 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | By providing the order method with a column name and order (default ascending) you can change the sort order. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.order(grid.schema[0].name, 'asc'); 25 | ``` 26 | 27 | ```json data.json 28 | [ 29 | { "col1": "foo", "col2": 0, "col3": "a" }, 30 | { "col1": "bar", "col2": 1, "col3": "b" }, 31 | { "col1": "baz", "col2": 2, "col3": "c" } 32 | ] 33 | ``` 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/docs/examples/28-select-an-area.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Select an area 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Select an area of the grid using a [`rect`](/reference/classes#rect) object. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.selectArea({ 25 | top: 2, 26 | bottom: 6, 27 | left: 1, 28 | right: 2, 29 | }); 30 | grid.draw(); 31 | ``` 32 | 33 | ```json data.json 34 | [ 35 | { "col1": "foo", "col2": 0, "col3": "a", "col4": "z" }, 36 | { "col1": "bar", "col2": 1, "col3": "b", "col4": "x" }, 37 | { "col1": "baz", "col2": 2, "col3": "c", "col4": "w" }, 38 | { "col1": "foo", "col2": 0, "col3": "a", "col4": "u" }, 39 | { "col1": "bar", "col2": 1, "col3": "b", "col4": "l" }, 40 | { "col1": "baz", "col2": 2, "col3": "c", "col4": "e" }, 41 | { "col1": "foo", "col2": 0, "col3": "a", "col4": "c" }, 42 | { "col1": "bar", "col2": 1, "col3": "b", "col4": "n" }, 43 | { "col1": "baz", "col2": 2, "col3": "c", "col4": "b" } 44 | ] 45 | ``` 46 | 47 | 48 | -------------------------------------------------------------------------------- /docs/docs/examples/29-remove-all-rows.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Remove all rows 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Just get rid of all the data. It is silly data anyway. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.data = []; 25 | 26 | ``` 27 | 28 | ```json data.json 29 | [ 30 | { "col1": "foo", "col2": 0, "col3": "a" }, 31 | { "col1": "bar", "col2": 1, "col3": "b" }, 32 | { "col1": "baz", "col2": 2, "col3": "c" } 33 | ] 34 | ``` 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/docs/examples/30-allow-new-rows.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Allow new rows 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Allow the input of new rows at the bottom of the grid. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | showNewRow: true, 21 | }); 22 | 23 | app.append(gridElement); 24 | ``` 25 | 26 | ```json data.json 27 | [{ "col1": "foo", "col2": 0, "col3": "a" }] 28 | ``` 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/docs/examples/31-create-a-web-component-grid.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create a web component grid 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | NB: This example is broken, the web component is not loaded in the sandpack config. 8 | 9 | Create a web component grid 10 | 11 | 12 | 13 | ```ts 14 | import canvasDatagrid from 'canvas-datagrid'; 15 | import data from '/data.json'; 16 | 17 | const app = document.getElementById('app'); 18 | const grid = document.createElement('canvas-datagrid'); 19 | 20 | app.appendChild(grid); 21 | ``` 22 | 23 | ```json data.json 24 | [ 25 | { "col1": "foo", "col2": 0, "col3": "a" }, 26 | { "col1": "bar", "col2": 1, "col3": "b" }, 27 | { "col1": "baz", "col2": 2, "col3": "c" } 28 | ] 29 | ``` 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/docs/examples/32-change-a-columns-title.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Change a columns's title 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Change a columns's title 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | showNewRow: true, 21 | }); 22 | 23 | app.append(gridElement); 24 | 25 | grid.schema[0].title = 'foo'; 26 | grid.draw(); 27 | ``` 28 | 29 | ```json data.json 30 | [ 31 | { "col1": "foo", "col2": 0, "col3": "a" }, 32 | { "col1": "bar", "col2": 1, "col3": "b" }, 33 | { "col1": "baz", "col2": 2, "col3": "c" } 34 | ] 35 | ``` 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/docs/examples/33-add-a-column.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Add a column 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | There is also an insert version of this function. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.addColumn({ 25 | defaultValue: function (e) { 26 | return 'Quux'; 27 | }, 28 | title: 'Bar', 29 | name: 'bar', 30 | }); 31 | ``` 32 | 33 | ```json data.json 34 | [ 35 | { "col1": "foo", "col2": 0, "col3": "a" }, 36 | { "col1": "bar", "col2": 1, "col3": "b" }, 37 | { "col1": "baz", "col2": 2, "col3": "c" } 38 | ] 39 | ``` 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/docs/examples/34-add-a-row.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Add a row 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | There is also an insert version of this function. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.addRow({ 25 | col1: 'a ' + new Date().toString(), 26 | col2: 'b ' + new Date().toString(), 27 | col3: 'c ' + new Date().toString(), 28 | }); 29 | ``` 30 | 31 | ```json data.json 32 | [ 33 | { "col1": "foo", "col2": 0, "col3": "a" }, 34 | { "col1": "bar", "col2": 1, "col3": "b" }, 35 | { "col1": "baz", "col2": 2, "col3": "c" } 36 | ] 37 | ``` 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/docs/examples/35-draw-html-via-event.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Draw HTML via event 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | This draws HTML into an SVG object, takes a picture of it and caches it into the grid, then draws it into the cell. In other words, it works, but it's slow. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.addEventListener('afterrendercell', function (e) { 25 | if (e.cell.columnIndex === 1 && e.cell.rowIndex > -1) { 26 | e.cell.innerHTML = 27 | '
' + 28 | e.cell.value + 29 | '
' + 30 | '
' + 31 | e.cell.value + 32 | '
'; 33 | } 34 | }); 35 | grid.draw(); 36 | ``` 37 | 38 | ```json data.json 39 | [ 40 | { "col1": "foo", "col2": 0, "col3": "a" }, 41 | { "col1": "bar", "col2": 1, "col3": "b" }, 42 | { "col1": "baz", "col2": 2, "col3": "c" } 43 | ] 44 | ``` 45 | 46 |
47 | -------------------------------------------------------------------------------- /docs/docs/examples/36-draw-html-via-data-type.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Draw HTML via data type 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | This draws HTML into an SVG object, takes a picture of it and caches it into the grid, then draws it into the cell. In other words, it works, but it\'s slow. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.schema = [ 25 | { name: 'id', type: 'number' }, 26 | { name: 'offendit', type: 'html' }, 27 | ]; 28 | grid.data = [ 29 | { id: 0, offendit: 'number' }, 30 | { id: 0, offendit: 'number' }, 31 | ]; 32 | grid.draw(); 33 | ``` 34 | 35 | ```json data.json 36 | [ 37 | { "col1": "foo", "col2": 0, "col3": "a" }, 38 | { "col1": "bar", "col2": 1, "col3": "b" }, 39 | { "col1": "baz", "col2": 2, "col3": "c" } 40 | ] 41 | ``` 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/docs/examples/37-open-a-tree.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Open a tree 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Open a tree grid on a specific row. Use the [`expandtree`](/reference/events#expantree) event to control the data and settings of the tree grid. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import createData from '/create-data'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | }); 20 | 21 | app.append(gridElement); 22 | 23 | grid.style.height = '300px'; 24 | grid.style.width = '100%'; 25 | grid.data = createData(); 26 | 27 | grid.addEventListener('expandtree', function expandTree(e) { 28 | e.treeGrid.data = createData(); 29 | // prevent repeated executions of this example code from blowing things up 30 | e.treeGrid.removeEventListener('expandtree', expandTree); 31 | }); 32 | grid.expandTree(2); 33 | ``` 34 | 35 | ```ts create-data.ts 36 | export default function () { 37 | var x, 38 | y, 39 | d = []; 40 | for (x = 0; x < 2000; x += 1) { 41 | d[x] = {}; 42 | for (y = 0; y < 20; y += 1) { 43 | d[x][y] = y * x; 44 | } 45 | } 46 | return d; 47 | } 48 | ``` 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/docs/examples/39-allow-users-to-open-trees.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Allow users to open trees 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Allows users to open trees. Use the `expandtree` event to control the data and settings of the tree grid. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import createData from '/create-data'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | tree: true, 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.style.height = '300px'; 25 | grid.style.width = '100%'; 26 | grid.data = createData(); 27 | 28 | function expandTree(e) { 29 | e.treeGrid.addEventListener('expandtree', expandTree); 30 | e.treeGrid.attributes.tree = true; 31 | e.treeGrid.data = createData(); 32 | } 33 | grid.addEventListener('expandtree', expandTree); 34 | ``` 35 | 36 | ```ts create-data.ts 37 | export default function () { 38 | var x, 39 | y, 40 | d = []; 41 | for (x = 0; x < 2000; x += 1) { 42 | d[x] = {}; 43 | for (y = 0; y < 20; y += 1) { 44 | d[x][y] = y * x; 45 | } 46 | } 47 | return d; 48 | } 49 | ``` 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/docs/examples/39-multiple-filters.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Multiple filters 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Create filters on more than one column at a time. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import createData from '/create-data'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | }); 20 | 21 | app.append(gridElement); 22 | 23 | grid.data = createData(); 24 | grid.setFilter('quick', '/the/i'); 25 | grid.setFilter('brown', 'quick'); 26 | ``` 27 | 28 | ```ts create-data.ts 29 | export default function () { 30 | const data = []; 31 | const words = [ 32 | 'The', 33 | 'quick', 34 | 'brown', 35 | 'fox', 36 | 'jumps', 37 | 'over', 38 | 'the', 39 | 'lazy', 40 | 'dog', 41 | ]; 42 | 43 | for (const x = 0; x < 1000; x += 1) { 44 | const row = {}; 45 | for (const idx = 0; idx < words.length; idx += 1) { 46 | row[words[idx]] = 47 | words[Math.floor(Math.random() * 1000) % (words.length - 1)]; 48 | } 49 | 50 | data.push(row); 51 | } 52 | 53 | return data; 54 | } 55 | ``` 56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/docs/examples/40-draw-a-picture.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Draw a picture 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Draw a picture into a cell. First hook into `rendertext` to show the text "No Image" text if there is no image. Then hook into `afterrendercell` to actually create an image element, hook into the image load event and draw the image once it\'s loaded.' 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | 14 | const app = document.getElementById('app'); 15 | const gridElement = document.createElement('div'); 16 | const imgs = {}; 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | }); 20 | 21 | app.append(gridElement); 22 | // stop the cell from rendering text 23 | grid.addEventListener('rendertext', function (e) { 24 | if (e.cell.rowIndex > -1) { 25 | if (e.cell.header.name === 'image') { 26 | e.cell.formattedValue = e.cell.value ? '' : 'No Image'; 27 | } 28 | } 29 | }); 30 | // after the cell is rendered, draw on top of it 31 | grid.addEventListener('afterrendercell', function (e) { 32 | var i, 33 | contextGrid = this; 34 | if (e.cell.header.name === 'image' && e.cell.value && e.cell.rowIndex > -1) { 35 | // if we haven't already made an image for this, do it now 36 | if (!imgs[e.cell.value]) { 37 | // create a new image object and store it in the imgs object 38 | i = imgs[e.cell.value] = new Image(); 39 | // get the image path from the cell's value 40 | i.src = e.cell.value; 41 | // when the image finally loads 42 | // call draw() again to run the else path 43 | i.onload = function (parentNode) { 44 | contextGrid.draw(); 45 | }; 46 | return; 47 | } 48 | // if we have an image already, draw it. 49 | i = imgs[e.cell.value]; 50 | if (i.width !== 0) { 51 | i.targetHeight = e.cell.height; 52 | i.targetWidth = e.cell.height * (i.width / i.height); 53 | e.ctx.drawImage(i, e.cell.x, e.cell.y, i.targetWidth, i.targetHeight); 54 | } 55 | } 56 | }); 57 | // add some images 58 | grid.data = [ 59 | { 60 | image: 'https://picsum.photos/seed/canvas/200', 61 | melius: 'causae', 62 | }, 63 | { 64 | image: 'https://picsum.photos/seed/datagrid/200', 65 | melius: 'omittam', 66 | }, 67 | { 68 | image: 'https://picsum.photos/seed/great/200', 69 | melius: 'explicari', 70 | }, 71 | { 72 | image: '', 73 | melius: 'principes', 74 | }, 75 | ]; 76 | // set the column widths and row heights 77 | grid.setColumnWidth(0, 300); 78 | grid.data.forEach(function (d, index) { 79 | grid.setRowHeight(index, 200); 80 | }); 81 | ``` 82 | 83 | 84 | -------------------------------------------------------------------------------- /docs/docs/examples/41-toggle-debug-data.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Toggle debug data 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Show fun debugging information. What are all these numbers? File a bug ticket to find out! 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import data from '/data.json'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data, 20 | debug: true, 21 | }); 22 | 23 | app.append(gridElement); 24 | ``` 25 | 26 | ```json data.json 27 | [ 28 | { "col1": "foo", "col2": 0, "col3": "a" }, 29 | { "col1": "bar", "col2": 1, "col3": "b" }, 30 | { "col1": "baz", "col2": 2, "col3": "c" } 31 | ] 32 | ``` 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/docs/examples/43-create-a-spreadsheet.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create a spreadsheet 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | It's just like excel, but without all the useful parts. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import createData from '/create-data'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data: createData(); 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.attributes.columnHeaderClickBehavior = 'select'; 25 | grid.style.columnHeaderCellHorizontalAlignment = 'center'; 26 | grid.style.height = '300px'; 27 | grid.style.width = '100%'; 28 | ``` 29 | 30 | ```ts create-data.ts 31 | export default function () { 32 | const data = []; 33 | function colName(n) { 34 | const ordA = 'a'.charCodeAt(0); 35 | const ordZ = 'z'.charCodeAt(0); 36 | const len = ordZ - ordA + 1; 37 | let s = ''; 38 | 39 | while (n >= 0) { 40 | s = String.fromCharCode((n % len) + ordA) + s; 41 | n = Math.floor(n / len) - 1; 42 | } 43 | 44 | return s; 45 | } 46 | 47 | for (const x = 0; x < 100; x += 1) { 48 | const row = {}; 49 | 50 | for (const y = 0; y < 50; y += 1) { 51 | const n = colName(y).toUpperCase(); 52 | row[n] = x * y; 53 | } 54 | 55 | data.push(row); 56 | } 57 | 58 | return data; 59 | } 60 | ``` 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/docs/examples/44-add-10000-random-rows.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Add 10,000 random rows 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | Because why not? 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import createData from '/create-data'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | }); 20 | 21 | app.append(gridElement); 22 | 23 | grid.style.height = '300px'; 24 | grid.style.width = '100%'; 25 | grid.data = createData(); 26 | ``` 27 | 28 | ```ts create-data.ts 29 | export default function () { 30 | const data = []; 31 | const words = 32 | 'Elend, eam, animal omittam an, has in, explicari principes. Elit, causae eleifend mea cu. No sed adipisci accusata, ei mea everti melius periculis. Ei quot audire pericula mea, qui ubique offendit no. Sint mazim mandamus duo ei. Sumo maiestatis id has, at animal reprehendunt definitionem cum, mei ne adhuc theophrastus.'; 33 | const wordsSplitBySpace = words.split(' ').map(function (i) { 34 | return i.trim(); 35 | }); 36 | const wordsSplitByComma = words.split(',').map(function (i) { 37 | return i.trim(); 38 | }); 39 | 40 | for (const x = 0; x < 10000; x += 1) { 41 | const row = {}; 42 | for (const idx = 0; idx < wordsSplitByComma.length; idx += 1) { 43 | row[wordsSplitByComma[idx]] = 44 | wordsSplitBySpace[ 45 | Math.floor(Math.random() * 1000) % (wordsSplitBySpace.length - 1) 46 | ]; 47 | } 48 | 49 | data.push(row); 50 | } 51 | 52 | return data; 53 | } 54 | ``` 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/docs/examples/45-disco-mode.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Disco Mode 3 | --- 4 | 5 | import siteConfig from '/docusaurus.config.js'; 6 | 7 | This is silly. There is no way to stop it. 8 | 9 | 10 | 11 | ```ts 12 | import canvasDatagrid from 'canvas-datagrid'; 13 | import createData from '/create-data'; 14 | 15 | const app = document.getElementById('app'); 16 | const gridElement = document.createElement('div'); 17 | const grid = canvasDatagrid({ 18 | parentNode: gridElement, 19 | data: createData(), 20 | }); 21 | 22 | app.append(gridElement); 23 | 24 | grid.style.height = '300px'; 25 | grid.style.width = '100%'; 26 | 27 | function getRandomColor() { 28 | function getRandomInt(min, max) { 29 | min = Math.ceil(min); 30 | max = Math.floor(max); 31 | return Math.floor(Math.random() * (max - min)) + min; 32 | } 33 | 34 | // rgb(235, 33, 177) 35 | return ( 36 | 'rgb(' + 37 | getRandomInt(0, 255) + 38 | ', ' + 39 | getRandomInt(25, 244) + 40 | ', ' + 41 | getRandomInt(0, 127) + 42 | ')' 43 | ); 44 | } 45 | grid.addEventListener('rendercell', function (e) { 46 | e.ctx.fillStyle = getRandomColor(); 47 | e.ctx.strokeStyle = getRandomColor(); 48 | }); 49 | 50 | setInterval(grid.draw, 500); 51 | grid.draw(); 52 | ``` 53 | 54 | ```ts create-data.ts 55 | export default function () { 56 | const data = []; 57 | 58 | for (const x = 0; x < 100; x += 1) { 59 | const row = {}; 60 | 61 | for (const y = 0; y < 20; y += 1) { 62 | row[y] = y * x; 63 | } 64 | 65 | data.push(row); 66 | } 67 | 68 | return data; 69 | } 70 | ``` 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | --- 4 | 5 | ## Installation 6 | 7 | 8 | With [npm](https://www.npmjs.com/package/canvas-datagrid) 9 | 10 | ```console 11 | npm install canvas-datagrid 12 | ``` 13 | 14 | Place the single source file `./dist/canvas-datagrid.js` in your web page using a script tag that points to the source or use a bundler. 15 | 16 | ```html 17 | 18 | ``` 19 | 20 | Alternatively, instead of downloading and installing, you can link directly to an NPM CDN like [unpkg.com](https://unpkg.com). 21 | 22 | ```html 23 | 24 | ``` 25 | 26 | A function will be added to the global scope of the web page called `canvasDatagrid` as well as module loader definitions. 27 | 28 | 29 | ## Basic Usage 30 | 31 | Works [with webpack](https://canvas-datagrid.js.org/amdDemo.html), [without webpack](https://canvas-datagrid.js.org/demo.html) or as a [web component](https://canvas-datagrid.js.org/webcomponentDemo.html). 32 | No matter how you load it, `canvasDatagrid` is declared in the global scope. 33 | 34 | Canvas-datagrid is a [Web Component](https://www.webcomponents.org/element/TonyGermaneri/canvas-datagrid) when 35 | in a compatible browser, otherwise it is a `` tag. 36 | 37 | 38 | ### Using pure Javascript 39 | 40 | ```js 41 | var grid = canvasDatagrid(); 42 | 43 | document.body.appendChild(grid); 44 | 45 | grid.data = [ 46 | { col1: 'row 1 column 1', col2: 'row 1 column 2', col3: 'row 1 column 3' }, 47 | { col1: 'row 2 column 1', col2: 'row 2 column 2', col3: 'row 2 column 3' }, 48 | ]; 49 | ``` 50 | 51 | ### Using as a web component 52 | 53 | ```html 54 | 55 | [ 56 | {"col1": "row 1 column 1", "col2": "row 1 column 2", "col3": "row 1 column 3"}, 57 | {"col1": "row 2 column 1", "col2": "row 2 column 2", "col3": "row 2 column 3"} 58 | ] 59 | 60 | ``` 61 | 62 | or 63 | 64 | ```js 65 | const grid = document.createElement('canvas-datagrid'); 66 | 67 | grid.data = [ 68 | { col1: 'row 1 column 1', col2: 'row 1 column 2', col3: 'row 1 column 3' }, 69 | { col1: 'row 2 column 1', col2: 'row 2 column 2', col3: 'row 2 column 3' }, 70 | ]; 71 | ``` 72 | ### Using Vue 73 | 74 | ```vue 75 | 76 | ``` 77 | 78 | ### More demos 79 | 80 | - [Using Vue](https://canvas-datagrid.js.org/vueExample.html) 81 | 82 | - [Using Webpack3: AMD](https://canvas-datagrid.js.org/amdDemo.html) 83 | 84 | - [Using React](https://canvas-datagrid.js.org/reactExample.html) 85 | 86 | - [Web component example](https://canvas-datagrid.js.org/webcomponentDemo.html) 87 | 88 | - [Loading data with XHR](https://canvas-datagrid.js.org/demo.html) 89 | 90 | - [Sparkline example](https://canvas-datagrid.js.org/sparklineDemo.html) 91 | 92 | - [XHR data paging demo Jeopardy Questions API](https://canvas-datagrid.js.org/xhrPagingDemo.html) 93 | 94 | Note about XHR paging demo: Thanks to [jservice](http://jservice.io/) for the use of the free paging API. You must "load unsafe scripts" or relevant command to allow HTTPS (github) to make XHR requests to HTTP (Jeopardy Questions API). There is nothing unsafe about this. 95 | 96 | -------------------------------------------------------------------------------- /docs/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | slug: /intro 4 | title: Introduction 5 | --- 6 | 7 | # canvas-datagrid 8 | 9 | ![](/img/datagrid1.png) 10 | 11 | canvas-datagrid is an HTML Canvas-based grid for spreadsheet-like display of data, like Excel or Google Sheets. Some features: 12 | 13 | - Works with Firefox, Edge, Safari and Chrome. 14 | - Native support for touch devices (phones and tablets). 15 | - Rich documentation, tutorials, and slack support. 16 | - Single canvas element, drawn in immediate mode, data size does not impact performance. 17 | - Support for unlimited rows and columns without paging or loading. 18 | - Rich API of events, methods and properties using the familiar W3C DOM interface. 19 | - Extensible styling, filtering, formatting, resizing, selecting, and ordering. 20 | - Support for hierarchal drill in style row level inner grids as well grids in cells. 21 | - Customizable hierarchal context menu. 22 | - Built in and custom styles. 23 | - W3C Web Component. Works in all frameworks. 24 | - Per-user styles, column sizes, row sizes, view preferences and settings using localStorage. 25 | - Small file size 26 | -------------------------------------------------------------------------------- /docs/docs/topics/drawing-on-the-canvas.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Drawing on the canvas 3 | --- 4 | 5 | Extending behavior is done using event handlers just like a regular HTML element. 6 | You can alter the content of a rendered cell by attaching to such an event handler. 7 | Below is an example of drawing an image into a cell: 8 | 9 | This example attaches to two events. `rendertext` to prevent the rendering of text into the cell... 10 | 11 | ```js 12 | grid.addEventListener('rendertext', function (e) { 13 | if (e.cell.rowIndex > -1) { 14 | if (e.cell.header.name === 'MyImageColumnName') { 15 | e.cell.formattedValue = e.cell.value ? '' : 'No Image'; 16 | } 17 | } 18 | }); 19 | ``` 20 | 21 | ... and `afterrendercell` to draw an image into the cell after the background and borders are drawn. 22 | Because the image is loaded asynchronously, you need to attach to the load event to actually draw 23 | the image. 24 | 25 | ```js 26 | var imgs = {}; 27 | 28 | grid.addEventListener('afterrendercell', function (e) { 29 | var i, 30 | contextGrid = this; 31 | 32 | if ( 33 | e.cell.header.name === 'MyImageColumnName' && 34 | e.cell.value && 35 | e.cell.rowIndex > -1 36 | ) { 37 | if (!imgs[e.cell.value]) { 38 | i = imgs[e.cell.value] = new Image(); 39 | i.src = e.cell.value; 40 | i.onload = function () { 41 | i.targetHeight = e.cell.height; 42 | i.targetWidth = e.cell.height * (i.width / i.height); 43 | contextGrid.draw(); 44 | }; 45 | return; 46 | } 47 | i = imgs[e.cell.value]; 48 | if (i.width !== 0) { 49 | e.ctx.drawImage(i, e.cell.x, e.cell.y, i.targetWidth, i.targetHeight); 50 | } 51 | } 52 | }); 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/docs/topics/extending-the-visual-appearance.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Extending the visual appearance 3 | --- 4 | 5 | All visual elements of the canvas are dependent on the values of the style object. 6 | Using the style object, you can change the dimensions and appearance of any element of the grid. 7 | 8 | There are two types of styles, styles built into the DOM element, such as width and margin, and there 9 | are styles related to the drawing of the grid on the canvas, these are listed in the style section. 10 | 11 | This example changes the fill style of the cell when the cell is a certain value. 12 | 13 | ```js 14 | grid.addEventListener('rendercell', function (e) { 15 | if (e.cell.header.name === 'MyStatusCell' && /blah/.test(e.cell.value)) { 16 | e.ctx.fillStyle = '#AEEDCF'; 17 | } 18 | }); 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/docs/topics/formatting-using-event-listeners.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Formatting using event listeners 3 | --- 4 | 5 | You can format the data in your cells without altering the data in two ways. 6 | 7 | The first and fastest method is grid formatters. 8 | Grid formatters allow you to pass your values though a function to format them as they are drawn onto the grid. 9 | Data type is defined in the [schema](https://canvas-datagrid.js.org/#schema) that you can optionally pass to describe your data. 10 | 11 | This method is slightly faster due to the O(1) hash map employed in the value formatters. 12 | 13 | In the following example, the type `date` is given a formatter function. 14 | 15 | ```js 16 | grid.formatters.date = function (e) { 17 | return new Date(e.cell.value).toISOString(); 18 | }; 19 | ``` 20 | 21 | The return value of the formatter function will be displayed in the cell instead of the value 22 | in the data without altering the data. 23 | 24 | The second method is the `rendertext` event. By subscribing to the `rendertext` event listener 25 | we can intercept the value in the context of the [cell](https://canvas-datagrid.js.org/#canvasDatagrid.cell) being drawn and alter it. 26 | 27 | This method is slightly slower due to the O(n) loop employed in the event handler class. 28 | 29 | This method is not dependent on values in the schema. This methods overrides `grid.formatters`. 30 | 31 | ```js 32 | grid.addEventListener('rendertext', function (e) { 33 | if (e.cell.rowIndex > -1) { 34 | if (e.cell.header.name === 'MyDateColumnName') { 35 | e.cell.formattedValue = new Date(e.cell.value).toISOString(); 36 | } 37 | } 38 | }); 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/docs/topics/setting-a-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setting a schema 3 | --- 4 | 5 | Schema is optional. Schema is an array of header objects. 6 | This documentation will use the term header and column interchangeably. 7 | If no schema is provided one will be generated from the 8 | data, in that case all data will be assumed to be string data. 9 | 10 | Note: When data is generated from an 2D array (array of arrays vs. array of objects) the columns will be named A, B, C, D,... etc.. 11 | 12 | Each header object can have the following properties: 13 | 14 | | Property | Description | 15 | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 16 | | name | The name of the column. This is used to match with data and is the only required property. | 17 | | type | The data type of this column | 18 | | title | What will be displayed to the user. If not present, name will be used. | 19 | | width | The default width in pixels of this column. | 20 | | hidden | When true the column will be hidden. | 21 | | filter | The filter function to use to filter this column. If no function is provided, type will determine filer. | 22 | | formatter | The formatter function used display this column. If no function is provided, type will determine formatter. | 23 | | defaultValue | The default value of this column for new rows. This is a function or a string. When a function is used, it takes the arguments `header` and `index` and must return a value for new default cell value in this column. | 24 | 25 | Example schema: 26 | 27 | ```js 28 | [ 29 | { 30 | name: 'col1', 31 | }, 32 | { 33 | name: 'col2', 34 | }, 35 | { 36 | name: 'col3', 37 | }, 38 | ]; 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/docs/topics/setting-and-getting-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setting and getting data 3 | --- 4 | 5 | Data is set according to the MIME type parser defined in grid.dataType. The default type parser is `application/x-canvas-datagrid`. 6 | 7 | This format expects an array of objects or an array of arrays that strictly conform to a schema (i.e.: they all have the same properties or lengths). 8 | 9 | Example `application/x-canvas-datagrid` 10 | 11 | ```js 12 | [ 13 | { col1: 'row 1 column 1', col2: 'row 1 column 2', col3: 'row 1 column 3' }, 14 | { col1: 'row 2 column 1', col2: 'row 2 column 2', col3: 'row 2 column 3' }, 15 | ]; 16 | ``` 17 | 18 | or 19 | 20 | ```js 21 | [ 22 | ['row 1 column 1', 'row 1 column 2', 'row 1 column 3'], 23 | ['row 2 column 1', 'row 2 column 2', 'row 2 column 3'], 24 | ]; 25 | ``` 26 | 27 | When getting data, no matter how it was set, it will be returned as `application/x-canvas-datagrid` (an array of objects). 28 | 29 | For more information on using and creating custom parsers see: [parsers](https://canvas-datagrid.js.org/#parsers) 30 | 31 | The table below lists ways to set data and the default parser used. 32 | 33 | | Method | Parser | 34 | | --------------------------------- | ---------------------------------- | 35 | | data property | application/x-canvas-datagrid | 36 | | web component data attribute | application/json+x-canvas-datagrid | 37 | | web component innerHTML attribute | application/json+x-canvas-datagrid | 38 | 39 | There are two built in parsers. 40 | 41 | application/x-canvas-datagrid (Default) 42 | application/json+x-canvas-datagrid 43 | 44 | Note: When setting data via the web component innerHTML attribute, only string data can be passed. 45 | 46 | Note: When you pass string data into the web component and the `grid.dataType` is set to the default: `application/x-canvas-datagrid` it will become set to `application/json+x-canvas-datagrid` to parse the string data. If `grid.dataType` was previously changed, the parser it was changed to will be used instead. 47 | -------------------------------------------------------------------------------- /docs/docs/topics/setting-height-and-width.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setting height and width 3 | --- 4 | 5 | Limiting the size of the canvas-datagrid element is the best way to improve performance. 6 | 7 | canvas-datagrid by default is set to `grid.style.height: auto` and `grid.style.width: auto`. 8 | This means the canvas-datagrid element expands to the height and width of the rows and columns contained within. 9 | If you have many rows or columns you will want to limit the height and width of the element by setting 10 | `grid.style.height` and `grid.style.width` to something besides `auto`. Try `100%` or `300px`. 11 | 12 | When set to a value other than auto a virtual scroll box will be drawn onto the canvas 13 | constraining the size and allowing the user to scroll around the columns and rows. 14 | 15 | Currently `max-width`, `max-height`, `min-width` and `min-height` are not supported and will do nothing, but support is planned. 16 | -------------------------------------------------------------------------------- /docs/docs/topics/ways-to-create-a-grid.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ways to create a grid 3 | --- 4 | 5 | - Web component by using the tag `` anywhere in your document. 6 | - Web component by running `var foo = document.createElement('canvas-datagrid')`. 7 | - Webpack3 universal module loader by adding one of many module loaders to your application code. See example: {@link https://canvas-datagrid.js.org/amdDemo.html}. 8 | - You can also load the grid by invoking the global method `var foo = canvasDatagrid();` See example: {@link https://canvas-datagrid.js.org/demo.html} 9 | 10 | If you create the grid using the non-web component model, you can attach the grid to an existing canvas by passing the canvas in as the `parentNode` when you instantiate the grid using the module loader or global versions. This is not possible when instantiating using `createElement` or markup. 11 | 12 | With the exception of attaching to an existing canvas, the grid will attempt to create a Shadow DOM element and attach a canvas within. 13 | 14 | - Support for attaching to existing canvas elements is experimental. 15 | 16 | - In browsers that do not support custom tags, a `` tag is used in place of the `` tag. 17 | 18 | - In browsers that do not support Shadow DOM, no shadow root will be created. In this mode cascading CSS can alter the grid, altering behavior in potentially breaking ways. Careful application of CSS is required. This can effect the grid in-line editing and context menus. 19 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require('prism-react-renderer/themes/github'); 5 | const darkCodeTheme = require('prism-react-renderer/themes/dracula'); 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: 'Canvas Datagrid', 10 | tagline: 11 | 'Canvas based data grid web component. Capable of displaying millions of contiguous hierarchical rows and columns without paging or loading, on a single canvas element.', 12 | url: 'https://canvas-datagrid.js.org', 13 | baseUrl: '/', 14 | onBrokenLinks: 'throw', 15 | onBrokenMarkdownLinks: 'warn', 16 | favicon: 'img/favicon.ico', 17 | organizationName: 'TonyGermaneri', // Usually your GitHub org/user name. 18 | projectName: 'canvas-datagrid', // Usually your repo name. 19 | 20 | presets: [ 21 | [ 22 | '@docusaurus/preset-classic', 23 | /** @type {import('@docusaurus/preset-classic').Options} */ 24 | ({ 25 | docs: { 26 | // path: 'docs', 27 | sidebarPath: 'sidebars.js', 28 | routeBasePath: '/', 29 | // Please change this to your repo. 30 | editUrl: 31 | 'https://github.com/TonyGermaneri/canvas-datagrid/edit/main/docs/', 32 | }, 33 | theme: { 34 | customCss: require.resolve('./css/custom.css'), 35 | }, 36 | }), 37 | ], 38 | ], 39 | 40 | themeConfig: 41 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 42 | ({ 43 | navbar: { 44 | title: 'Canvas Datagrid', 45 | // logo: { 46 | // alt: 'My Site Logo', 47 | // src: 'img/logo.svg', 48 | // }, 49 | items: [ 50 | { 51 | type: 'doc', 52 | docId: 'intro', 53 | position: 'right', 54 | label: 'Introduction', 55 | }, 56 | { 57 | to: 'getting-started', 58 | label: 'Getting started', 59 | position: 'right', 60 | }, 61 | { 62 | to: 'examples', 63 | label: 'Examples', 64 | position: 'right', 65 | }, 66 | { 67 | href: 'https://github.com/TonyGermaneri/canvas-datagrid', 68 | label: 'GitHub', 69 | position: 'right', 70 | }, 71 | ], 72 | }, 73 | footer: { 74 | style: 'dark', 75 | links: [ 76 | { 77 | title: 'Docs', 78 | items: [ 79 | { 80 | label: 'Introduction', 81 | to: 'intro', 82 | }, 83 | { 84 | to: 'getting-started', 85 | label: 'Getting started', 86 | }, 87 | { 88 | to: 'examples', 89 | label: 'Examples', 90 | }, 91 | ], 92 | }, 93 | { 94 | title: 'Community', 95 | items: [ 96 | { 97 | label: 'Slack support', 98 | href: 'https://canvas-datagrid.slack.com/', 99 | }, 100 | ], 101 | }, 102 | { 103 | title: 'More', 104 | items: [ 105 | { 106 | label: 'GitHub', 107 | href: 'https://github.com/TonyGermaneri/canvas-datagrid', 108 | }, 109 | { 110 | href: 'https://www.npmjs.com/package/canvas-datagrid', 111 | label: 'npm', 112 | }, 113 | { 114 | href: 'https://canvas-datagrid.js.org/', 115 | label: 'js.org', 116 | }, 117 | ], 118 | }, 119 | ], 120 | copyright: `Copyright © ${new Date().getFullYear()} Canvas Datagrid`, 121 | }, 122 | prism: { 123 | theme: lightCodeTheme, 124 | darkTheme: darkCodeTheme, 125 | }, 126 | }), 127 | }; 128 | 129 | module.exports = config; 130 | -------------------------------------------------------------------------------- /docs/jsdoc-to-md.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Handlebars = require('handlebars'); 4 | const jsdocApi = require('jsdoc-api'); 5 | const fs = require('fs'); 6 | const fsExtra = require('fs-extra'); 7 | const path = require('path'); 8 | 9 | // As far as I know, these are the only files containing JSDoc annotations: 10 | const inputFiles = [ 11 | path.join(__dirname, '..', 'lib', 'docs.js'), 12 | path.join(__dirname, '..', 'lib', 'publicMethods.js'), 13 | ]; 14 | 15 | const outputDir = path.join(__dirname, 'docs', 'reference'); 16 | const templateDir = path.join(__dirname, 'templates'); 17 | 18 | const sortByProp = (prop) => (a, b) => a[prop].localeCompare(b[prop]); 19 | 20 | // This is more for development environments: if we change our docs.js, 21 | // we don't want any dangling markdown files left in the directory. For 22 | // publication on canvas-datagrid.js.org this is not relevant, as that 23 | // environment is created anew each time. 24 | fsExtra.emptyDirSync(outputDir); 25 | 26 | /* parse jsdoc data */ 27 | const data = jsdocApi.explainSync({ files: inputFiles }); 28 | 29 | function getEvents(jsdocData) { 30 | const events = jsdocData.filter( 31 | (identifier) => 32 | identifier.kind === 'event' && identifier.memberof === 'canvasDatagrid', 33 | ); 34 | 35 | return events.sort(sortByProp('name')); 36 | } 37 | 38 | function getParams(jsdocData) { 39 | const cdgNode = jsdocData.find( 40 | (identifier) => identifier.name === 'canvasDatagrid', // && identifier.kind === 'constructor', 41 | ); 42 | const params = cdgNode.params.filter((p) => p.name !== 'args'); // exclude non-param description 43 | 44 | return params 45 | .map((p) => ({ ...p, name: p.name.replace(/^args\./, '') })) 46 | .sort(sortByProp('name')); 47 | } 48 | 49 | function getProperties(jsdocData) { 50 | const cdgNode = jsdocData.find( 51 | (identifier) => 52 | identifier.name === 'canvasDatagrid' && identifier.kind === 'class', 53 | ); 54 | const properties = cdgNode.properties; 55 | 56 | return properties.sort(sortByProp('name')); 57 | } 58 | 59 | function getClasses(jsdocData) { 60 | const classes = jsdocData.filter((identifier) => identifier.kind === 'class'); 61 | 62 | return classes; 63 | } 64 | 65 | function getMethods(jsdocData) { 66 | const cdgNode = jsdocData.find( 67 | (identifier) => identifier.name === 'canvasDatagrid', 68 | ); 69 | 70 | return jsdocData 71 | .filter((identifier) => identifier.kind === 'function') 72 | .concat(cdgNode.methods); 73 | } 74 | 75 | function getStyles(jsdocData) { 76 | const style = jsdocData.find( 77 | (identifier) => 78 | identifier.kind === 'class' && 79 | identifier.longname === 'canvasDatagrid.style', 80 | ); 81 | 82 | return style.properties.sort(sortByProp('name')); 83 | } 84 | 85 | function writeMarkdown({ outputFile, templateFile, data }) { 86 | const templateString = fsExtra.readFileSync(templateFile).toString(); 87 | const template = Handlebars.compile(templateString); 88 | const outputString = template({ data }); 89 | 90 | fsExtra.writeFileSync(outputFile, outputString); 91 | } 92 | 93 | const sections = { 94 | parameters: { data: getParams(data), template: 'parameters.hbs' }, 95 | properties: { data: getProperties(data), template: 'properties.hbs' }, 96 | events: { data: getEvents(data), template: 'events.hbs' }, 97 | classes: { data: getClasses(data), template: 'classes.hbs' }, 98 | methods: { data: getMethods(data), template: 'methods.hbs' }, 99 | styling: { data: getStyles(data), template: 'styling.hbs' }, 100 | }; 101 | 102 | for (const [sectionName, section] of Object.entries(sections)) { 103 | writeMarkdown({ 104 | outputFile: path.join(outputDir, `${sectionName}.md`), 105 | templateFile: path.join(templateDir, `${sectionName}.hbs`), 106 | data: section.data, 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "prebuild": "node jsdoc-to-md.js", 9 | "build": "docusaurus build", 10 | "swizzle": "docusaurus swizzle", 11 | "deploy": "docusaurus deploy", 12 | "clear": "docusaurus clear", 13 | "serve": "docusaurus serve", 14 | "write-translations": "docusaurus write-translations", 15 | "write-heading-ids": "docusaurus write-heading-ids" 16 | }, 17 | "dependencies": { 18 | "@codesandbox/sandpack-react": "1.19.0", 19 | "@docusaurus/core": "2.4.0", 20 | "@docusaurus/preset-classic": "2.4.0", 21 | "@mdx-js/react": "1.6.22", 22 | "@svgr/webpack": "6.5.1", 23 | "clsx": "1.2.1", 24 | "file-loader": "6.2.0", 25 | "fs-extra": "10.0.0", 26 | "handlebars": "4.7.7", 27 | "jsdoc-api": "7.1.0", 28 | "prism-react-renderer": "1.3.5", 29 | "react": "17.0.2", 30 | "react-dom": "17.0.2", 31 | "url-loader": "4.1.1" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.5%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | 14 | // @ts-check 15 | 16 | function allFiles(dirname) { 17 | return fs.readdirSync(dirname); 18 | } 19 | 20 | function allMarkdownSlugs(dirname) { 21 | return allFiles(dirname) 22 | .filter((filename) => filename.endsWith('.md')) 23 | .map((filename) => filename.replace(/\.md$/, '')); 24 | } 25 | 26 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 27 | const sidebars = { 28 | docs: [ 29 | 'intro', 30 | 'getting-started', 31 | 'building-and-testing', 32 | { 33 | type: 'category', 34 | label: 'Topics', 35 | items: [ 36 | 'topics/drawing-on-the-canvas', 37 | 'topics/extending-the-visual-appearance', 38 | 'topics/formatting-using-event-listeners', 39 | 'topics/setting-a-schema', 40 | 'topics/setting-and-getting-data', 41 | 'topics/setting-height-and-width', 42 | 'topics/ways-to-create-a-grid', 43 | ], 44 | }, 45 | { 46 | type: 'category', 47 | label: 'Reference', 48 | items: [ 49 | 'reference/properties', 50 | 'reference/parameters', 51 | 'reference/events', 52 | 'reference/methods', 53 | 'reference/classes', 54 | 'reference/styling', 55 | ], 56 | }, 57 | { 58 | type: 'category', 59 | label: 'Examples', 60 | link: { type: 'doc', id: 'examples' }, 61 | items: [ 62 | { 63 | type: 'autogenerated', 64 | dirName: 'examples', 65 | }, 66 | ], 67 | }, 68 | ], 69 | }; 70 | 71 | module.exports = sidebars; 72 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import styles from './HomepageFeatures.module.css'; 4 | 5 | const FeatureList = [ 6 | { 7 | title: 'High Performance', 8 | Svg: require('../../static/img/high-performance.svg').default, 9 | description: ( 10 | <> 11 | Canvas-datagrid is an extremely fast library for large datasets, 12 | particularly for financial or scientific data, and uses performance 13 | optimization for smooth scrolling and responsive interactions. 14 | 15 | ), 16 | }, 17 | { 18 | title: 'Customizable', 19 | Svg: require('../../static/img/customizable.svg').default, 20 | description: ( 21 | <> 22 | Canvas-datagrid offers a wide range of customization options to match 23 | your application's design, including color scheme, font, cell sizes and 24 | custom cell renderers and editors. 25 | 26 | ), 27 | }, 28 | { 29 | title: 'Interactive', 30 | Svg: require('../../static/img/interactive.svg').default, 31 | description: ( 32 | <> 33 | Canvas-datagrid provides interactive features like navigation, sorting, 34 | filtering, cell selection, editing and keyboard shortcuts making it easy 35 | for users to perform common actions 36 | 37 | ), 38 | }, 39 | ]; 40 | 41 | function Feature({ Svg, title, description }) { 42 | return ( 43 |
44 |
45 | 46 |
47 |
48 |

{title}

49 |

{description}

50 |
51 |
52 | ); 53 | } 54 | 55 | export default function HomepageFeatures() { 56 | return ( 57 |
58 |
59 |
60 | {FeatureList.map((props, idx) => ( 61 | 62 | ))} 63 |
64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/components/SandpackEditor/createFileMap.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { SandpackFile } from "@codesandbox/sandpack-react"; 3 | 4 | export const createFileMap = ( 5 | children: JSX.Element 6 | ): Record => { 7 | let codeSnippets = React.Children.toArray(children) as React.ReactElement[]; 8 | 9 | return codeSnippets.reduce( 10 | (result: Record, codeSnippet: React.ReactElement) => { 11 | if (codeSnippet.props.mdxType !== "pre") { 12 | return result; 13 | } 14 | const { props } = codeSnippet.props.children; 15 | 16 | /* 17 | If no metastring is provided src/index.ts overrides the file in the vanilla-ts template structure. 18 | If we want to support other templates file paths for these file paths need to be added here. 19 | */ 20 | const filepath = props.metastring ? `/${props.metastring}` : '/src/index.ts'; 21 | 22 | result[filepath] = props.children; 23 | 24 | return result; 25 | }, 26 | {} 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /docs/src/components/SandpackEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | SandpackProvider, 4 | SandpackCodeEditor, 5 | SandpackPreview, 6 | SandpackPredefinedTemplate 7 | } from "@codesandbox/sandpack-react"; 8 | 9 | import { createFileMap } from "./createFileMap"; 10 | 11 | import canvasDatagridPackage from "../../../../package.json" 12 | 13 | const version = canvasDatagridPackage.version; 14 | 15 | export default ({ 16 | children, 17 | dependencies = {}, 18 | }: { 19 | template: SandpackPredefinedTemplate; 20 | children: JSX.Element, 21 | dependencies: { [key: string]: string }, 22 | }) => { 23 | 24 | return ( 25 | 40 | 41 | 42 | 43 | ); 44 | }; -------------------------------------------------------------------------------- /docs/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Link from '@docusaurus/Link'; 4 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 | import Layout from '@theme/Layout'; 6 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 7 | 8 | import styles from './index.module.css'; 9 | 10 | function HomepageHeader() { 11 | const { siteConfig } = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 |

{siteConfig.title}

16 |

{siteConfig.tagline}

17 |
18 | 22 | Try it now 23 | 24 |
25 |
26 |
27 | ); 28 | } 29 | 30 | export default function Home() { 31 | const { siteConfig } = useDocusaurusContext(); 32 | return ( 33 | 34 | 35 |
36 | 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 966px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/src/theme/MDXComponents.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MDXComponents from '@theme-original/MDXComponents'; 3 | import SandpackEditor from '@site/src/components/SandpackEditor'; 4 | 5 | export default { 6 | ...MDXComponents, 7 | SandpackEditor: SandpackEditor, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonyGermaneri/canvas-datagrid/c3e25fe5a5cbe8412ff92702ce384212c7b337cf/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/customizable.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/static/img/datagrid1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonyGermaneri/canvas-datagrid/c3e25fe5a5cbe8412ff92702ce384212c7b337cf/docs/static/img/datagrid1.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonyGermaneri/canvas-datagrid/c3e25fe5a5cbe8412ff92702ce384212c7b337cf/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/high-performance.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/static/img/interactive.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/templates/classes.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | title: Classes 3 | --- 4 | 5 | {{#each data}} 6 | ### {{name}} 7 | 8 | {{{description}}} 9 | 10 | #### Properties 11 | 12 | |Name|Type|Description| 13 | |---|---|---| 14 | {{#each properties}}|{{name}}|{{type.names}}|{{{description}}}| 15 | {{/each}} 16 | {{/each}} -------------------------------------------------------------------------------- /docs/templates/events.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | title: Events 3 | --- 4 | 5 | This document describes all events emitted by canvas-datagrid. Use the sidebar to the right to quickly find the event you're looking for. 6 | 7 | Listening for events can be done by registering a listener: 8 | 9 | ```js 10 | const grid = canvasDatagrid(); 11 | 12 | function renderTextHandler(e) { 13 | // your code here; 'e' is the event object 14 | } 15 | 16 | grid.addEventListener('rendertext', renderTextHandler); 17 | ``` 18 | 19 | Don't forget to stop listening when you destroy the grid: 20 | 21 | ```js 22 | grid.removeEventListener('rendertext', renderTextHandler); 23 | ``` 24 | 25 | 26 | ## All events 27 | 28 | {{#each data}} 29 | ### {{this.name}} 30 | 31 | {{{this.description}}} 32 | 33 | |Property name|Type|Description| 34 | |---|---|---| 35 | {{#each this.params}} 36 | |{{name}}|{{type.names}}|{{{description}}}| 37 | {{/each}} 38 | 39 | {{/each}} -------------------------------------------------------------------------------- /docs/templates/methods.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | title: Methods 3 | --- 4 | 5 | {{#each data}} 6 | ### {{name}} 7 | 8 | {{{description}}} 9 | 10 | |Name|Type|Optional|Default|Description| 11 | |---|---|---|---|---| 12 | {{#each params}} 13 | |{{name}}|{{type.names}}|{{optiona}}|{{defaultValue}}|{{{description}}}| 14 | {{/each}} 15 | 16 | {{/each}} -------------------------------------------------------------------------------- /docs/templates/parameters.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | title: Options 3 | --- 4 | 5 | canvas-datagrid is highly configurable. This document describes all options you can pass to the constructor: 6 | 7 | ```js 8 | const grid = canvasDatagrid({ 9 | data: myData, 10 | // other options 11 | }); 12 | ``` 13 | 14 | ## All options 15 | 16 | {{#each data}} 17 | ### {{name}} 18 | 19 | {{{description}}} 20 | 21 | |Type|Optional|Default| 22 | |---|---|---| 23 | |{{type.names}}|{{optional}}|` {{{defaultvalue}}} ` 24 | 25 | {{/each}} 26 | -------------------------------------------------------------------------------- /docs/templates/properties.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | title: Properties 3 | --- 4 | 5 | Once instantiated, canvas-datagrid exposes a number of properties. 6 | 7 | ```js 8 | const grid = canvasDatagrid({ data }); 9 | 10 | // Some user interaction occurs 11 | 12 | const activeCell = grid.activeCell; 13 | ``` 14 | 15 | ## All options 16 | 17 | |Name|Type|Description| 18 | |---|---|---| 19 | {{#each data}} 20 | 21 | ### {{name}} {{type.names}} 22 | 23 | {{{description}}} 24 | {{/each}} 25 | -------------------------------------------------------------------------------- /docs/templates/styling.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | title: Styling 3 | --- 4 | 5 | All visual elements of the canvas are dependent on the values of the style object. 6 | Using the style object, you can change the dimensions and appearance of any element of the grid. 7 | 8 | There are two types of styles, styles built into the DOM element, such as width and margin, and there 9 | are styles related to the drawing of the grid on the canvas, these are listed in the style section. 10 | 11 | Styles can be set during instantiation. 12 | 13 | ```js 14 | const grid = canvasDatagrid({ 15 | style: { 16 | gridBackgroundColor: 'red' 17 | } 18 | }); 19 | ``` 20 | 21 | Styles can be set after instantiation. 22 | 23 | ```js 24 | grid.style.gridBackgroundColor = 'red'; 25 | ``` 26 | 27 | When using the web component, styles can be set as above, but also using standard CSS. 28 | 29 | When using standard CSS, style names are hyphenated, lower case, and prefixed with `--cdg-`. 30 | 31 | ```html 32 | [{"my": "data"}] 33 | ``` 34 | 35 | When using the web component you can also use CSS classes and selectors as you would a native HTML element. 36 | 37 | ```html 38 | 43 | 44 | [{"my": "data"}] 45 | ``` 46 | 47 | You can build your own styles using the Style Builder. 48 | 49 | ## Style properties 50 | 51 | {{#each data}} 52 | ### {{name}} 53 | **{{type.names}}** 54 | 55 | 56 | {{{description}}} 57 | {{/each}} -------------------------------------------------------------------------------- /images/datagrid1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonyGermaneri/canvas-datagrid/c3e25fe5a5cbe8412ff92702ce384212c7b337cf/images/datagrid1.png -------------------------------------------------------------------------------- /jsdoc.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | source: { 5 | include: './lib/', 6 | }, 7 | opts: { 8 | recurse: true, 9 | template: './tutorials', 10 | verbose: true, 11 | }, 12 | templates: { 13 | theme: 'simplex', 14 | syntaxTheme: 'dark', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /jsdoc.ts.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | source: { 5 | include: './lib/', 6 | }, 7 | plugins: ['./node_modules/tsd-jsdoc/dist/plugin'], 8 | opts: { 9 | recurse: true, 10 | template: './node_modules/tsd-jsdoc/dist', 11 | verbose: true, 12 | destination: 'dist', 13 | }, 14 | templates: { 15 | theme: 'simplex', 16 | syntaxTheme: 'dark', 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | browserNoActivityTimeout: 180000, 4 | 5 | // base path that will be used to resolve all patterns (eg. files, exclude) 6 | basePath: '', 7 | 8 | // frameworks to use 9 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 10 | frameworks: ['mocha', 'chai'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | { 15 | pattern: 'test/**/*.js', 16 | type: 'module', 17 | included: true, 18 | served: true, 19 | }, 20 | { 21 | pattern: 'lib/**/util.js', 22 | type: 'module', 23 | included: true, 24 | served: true, 25 | }, 26 | { 27 | pattern: 'dist/canvas-datagrid.debug.js', 28 | included: true, 29 | served: true, 30 | }, 31 | ], 32 | 33 | // list of files / patterns to exclude 34 | exclude: [], 35 | 36 | // preprocess matching files before serving them to the browser 37 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 38 | preprocessors: { 39 | '**/lib/*.js': ['coverage'], 40 | }, 41 | 42 | failOnFailingTestSuite: true, 43 | 44 | customContextFile: 'test/index.html', 45 | 46 | // test results reporter to use 47 | // possible values: 'dots', 'progress' 48 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 49 | reporters: ['progress', 'coverage'], 50 | 51 | coverageReporter: { 52 | type: 'json', 53 | dir: './build', 54 | file: 'coverage.json', 55 | }, 56 | 57 | // web server port 58 | port: 9876, 59 | 60 | // enable / disable colors in the output (reporters and logs) 61 | colors: true, 62 | 63 | // level of logging 64 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 65 | logLevel: config.LOG_INFO, 66 | 67 | // enable / disable watching file and executing tests whenever any file changes 68 | autoWatch: true, 69 | 70 | // start these browsers 71 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 72 | browsers: ['ChromeHeadless'], 73 | 74 | // Continuous Integration mode 75 | // if true, Karma captures browsers, runs the tests and exits 76 | singleRun: true, 77 | 78 | // Concurrency level 79 | // how many browser should be started simultaneous 80 | concurrency: Infinity, 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /lib/events/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isSupportedHtml = function (pasteValue) { 4 | // We need to match new lines in the HTML, .* won't match new line characters. 5 | // `s` regex modifier can't be used with `ecmaVersion === 2017`. 6 | // As a workaround using [\s\S]*. Fix when we upgrade `ecmaVersion`. 7 | const genericDiv = /(?:^(]*>)?[\s\S]*]*>)/; 8 | const genericSpan = /(?:^(]*>)?[\s\S]*]*>)/; 9 | const genericTable = /(?:^(]*>)?[\s\S]*]*>)/; // Matches Google Sheets format clipboard data format too. 10 | const excelTable = /(?:[\s\S]*]*>)/; 11 | const excelTableRow = /(?:[\s\S]*]*>)/; 12 | 13 | return [ 14 | genericDiv, 15 | genericTable, 16 | genericSpan, 17 | excelTable, 18 | excelTableRow, 19 | ].some((expression) => expression.test(pasteValue)); 20 | }; 21 | 22 | // Explanation of nodeType here: https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType 23 | const IGNORE_NODETYPES = [8, 3]; // '#text' and '#comment' 24 | 25 | const isHtmlTable = function (pasteValue) { 26 | return /(?:]*>)|(?:)/.test(pasteValue); 27 | }; 28 | 29 | const sanitizeElementData = function (element) { 30 | // It is not entirely clear if this check on nodeType is required. 31 | let elementData = element.nodeType === 1 ? element.innerText : element.data; 32 | 33 | return String(elementData).replace(/\s+/g, ' ').trim(); 34 | }; 35 | 36 | const parseHtmlText = function (data) { 37 | const doc = new DOMParser().parseFromString(data, 'text/html'); 38 | const element = doc.querySelector('div') || doc.querySelector('span'); 39 | const elementData = sanitizeElementData(element); 40 | 41 | return elementData 42 | .split('\n') 43 | .map((item) => item.split('\t').map((value) => ({ value: [{ value }] }))); 44 | }; 45 | 46 | const parseHtmlTable = function (data) { 47 | const doc = new DOMParser().parseFromString(data, 'text/html'); 48 | const trs = doc.querySelectorAll('table tr'); 49 | const rows = []; 50 | 51 | for (const tr of trs) { 52 | const row = []; 53 | 54 | for (const childNode of tr.childNodes) { 55 | if (IGNORE_NODETYPES.includes(childNode.nodeType)) continue; 56 | 57 | const col = { value: [] }; 58 | const value = sanitizeElementData(childNode); 59 | 60 | if (value) col.value.push({ value }); 61 | 62 | row.push(col); 63 | } 64 | 65 | rows.push(row); 66 | } 67 | 68 | return rows; 69 | }; 70 | 71 | const parseText = function (data) { 72 | return data 73 | .split('\n') 74 | .map((item) => item.split('\t').map((value) => ({ value: [{ value }] }))); 75 | }; 76 | 77 | const parseData = function (data, mimeType) { 78 | if (mimeType === 'text/html' && isHtmlTable(data)) { 79 | return parseHtmlTable(data); 80 | } else if (mimeType === 'text/html') { 81 | return parseHtmlText(data); 82 | } 83 | 84 | // Default data format is string, so split on new line, 85 | // and then enclose in an array (a row with one cell): 86 | return parseText(data); 87 | }; 88 | 89 | const htmlSafe = function (value) { 90 | if (typeof value !== 'string') return value; 91 | 92 | return value.replace(//g, '>'); 93 | }; 94 | 95 | const createTextString = function (selectedData, isNeat) { 96 | // Selected like [[0, 1], [0, 1]] of [[0, 3]] is neat; Selected like [[0, 1], [1, 2]] is untidy. 97 | // If not isNeat we just return a simple string of concatenated values. 98 | if (!isNeat) 99 | return selectedData.map((row) => Object.values(row).join('')).join(''); 100 | 101 | // If isNeat, we can create tab separated mutti-line text. 102 | return selectedData.map((row) => Object.values(row).join('\t')).join('\n'); 103 | }; 104 | 105 | const createHTMLString = function (selectedData, isNeat) { 106 | if (!isNeat) return createTextString(selectedData, isNeat); 107 | 108 | // If isNeat, we can create a HTML table with the selected data. 109 | let htmlString = ''; 110 | htmlString += selectedData 111 | .map( 112 | (row) => 113 | '' + 114 | Object.values(row) 115 | .map((value) => [''].join('')) 116 | .join('') + 117 | '', 118 | ) 119 | .join(''); 120 | htmlString += '
', htmlSafe(value), '
'; 121 | 122 | return htmlString; 123 | }; 124 | 125 | export { 126 | createTextString, 127 | createHTMLString, 128 | isSupportedHtml, 129 | htmlSafe, 130 | parseData, 131 | parseHtmlTable, 132 | parseHtmlText, 133 | parseText, 134 | sanitizeElementData, 135 | }; 136 | -------------------------------------------------------------------------------- /lib/groups/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Merge a new hidden row range into existed ranges array 5 | * @param {any[]} hiddenRowRanges tuples: Array<[bgeinRowIndex, endRowIndex]> 6 | * @param {number[]} newRange tuple: [beginRowIndex, endRowIndex] 7 | * @returns {boolean} 8 | */ 9 | const mergeHiddenRowRanges = function (hiddenRowRanges, newRange) { 10 | const [beginRowIndex, endRowIndex] = newRange; 11 | if (endRowIndex < beginRowIndex) return false; 12 | let inserted = false; 13 | for (let i = 0; i < hiddenRowRanges.length; i++) { 14 | const range = hiddenRowRanges[i]; 15 | if (beginRowIndex > range[1] + 1) continue; 16 | if (beginRowIndex <= range[0] && endRowIndex >= range[0]) { 17 | hiddenRowRanges[i] = [beginRowIndex, Math.max(endRowIndex, range[1])]; 18 | inserted = true; 19 | break; 20 | } 21 | if (beginRowIndex >= range[0]) { 22 | hiddenRowRanges[i] = [range[0], Math.max(endRowIndex, range[1])]; 23 | inserted = true; 24 | break; 25 | } 26 | } 27 | if (!inserted) hiddenRowRanges.push([beginRowIndex, endRowIndex]); 28 | // merge intersections after sorting ranges 29 | hiddenRowRanges.sort((a, b) => a[0] - b[0]); 30 | for (let i = 0; i < hiddenRowRanges.length - 1; i++) { 31 | const range = hiddenRowRanges[i]; 32 | const nextRange = hiddenRowRanges[i + 1]; 33 | if (nextRange[0] <= range[1] + 1) { 34 | hiddenRowRanges[i] = [range[0], Math.max(range[1], nextRange[1])]; 35 | hiddenRowRanges.splice(i + 1, 1); 36 | i--; 37 | } 38 | } 39 | return true; 40 | }; 41 | 42 | export { mergeHiddenRowRanges }; 43 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, unparam: true, todo: true, evil: true*/ 2 | /*globals Reflect: false, HTMLElement: true, define: true, MutationObserver: false, requestAnimationFrame: false, performance: false, btoa: false*/ 3 | 'use strict'; 4 | 5 | import component from './component'; 6 | import defaults from './defaults'; 7 | import draw from './draw'; 8 | import events from './events'; 9 | import touch from './touch'; 10 | import intf from './intf'; 11 | import selections from './selections/index'; 12 | import contextMenu from './contextMenu'; 13 | import button from './button'; 14 | import dom from './dom'; 15 | import publicMethods from './publicMethods'; 16 | 17 | var webComponent = component(); 18 | 19 | var modules = [ 20 | defaults, 21 | draw, 22 | events, 23 | touch, 24 | intf, 25 | selections, 26 | contextMenu, 27 | button, 28 | dom, 29 | publicMethods, 30 | ]; 31 | 32 | function Grid(args) { 33 | args = args || {}; 34 | var self = {}; 35 | self.isComponent = args.component === undefined; 36 | self.isChildGrid = 37 | args.parentNode && 38 | /canvas-datagrid-(cell|tree)/.test(args.parentNode.nodeType); 39 | if (self.isChildGrid) { 40 | self.intf = {}; 41 | } else { 42 | self.intf = self.isComponent 43 | ? eval('Reflect.construct(HTMLElement, [], new.target)') 44 | : document.createElement('canvas'); 45 | } 46 | self.args = args; 47 | self.intf.args = args; 48 | self.applyComponentStyle = webComponent.applyComponentStyle; 49 | self.hyphenateProperty = webComponent.hyphenateProperty; 50 | self.dehyphenateProperty = webComponent.dehyphenateProperty; 51 | self.createGrid = function grid(args) { 52 | args.component = false; 53 | return new Grid(args); 54 | }; 55 | 56 | modules.forEach(function (module) { 57 | module(self); 58 | }); 59 | 60 | if (self.isChildGrid) { 61 | self.shadowRoot = args.parentNode.shadowRoot; 62 | self.parentNode = args.parentNode; 63 | } else { 64 | self.shadowRoot = self.intf.attachShadow({ mode: 'open' }); 65 | self.parentNode = self.shadowRoot; 66 | } 67 | self.init(); 68 | return self.intf; 69 | } 70 | if (window.HTMLElement) { 71 | Grid.prototype = Object.create(window.HTMLElement.prototype); 72 | } 73 | // export web component 74 | if (window.customElements) { 75 | Grid.observedAttributes = webComponent.getObservableAttributes(); 76 | Grid.prototype.disconnectedCallback = webComponent.disconnectedCallback; 77 | Grid.prototype.attributeChangedCallback = 78 | webComponent.attributeChangedCallback; 79 | Grid.prototype.connectedCallback = webComponent.connectedCallback; 80 | Grid.prototype.adoptedCallback = webComponent.adoptedCallback; 81 | window.customElements.define('canvas-datagrid', Grid); 82 | } 83 | 84 | // export global 85 | if ( 86 | window && 87 | !window.canvasDatagrid && 88 | !window.require && 89 | // Present to exclude global declarations from ES Module bundles 90 | !window.EXCLUDE_GLOBAL 91 | ) { 92 | window.canvasDatagrid = function (args) { 93 | return new Grid(args); 94 | }; 95 | } 96 | 97 | // export amd loader 98 | export default function canvasDatagrid(args) { 99 | args = args || {}; 100 | var i, 101 | tKeys = [ 102 | 'style', 103 | 'formatters', 104 | 'sorters', 105 | 'filters', 106 | 'treeGridAttributes', 107 | 'cellGridAttributes', 108 | 'fillCellCallback', 109 | 'data', 110 | 'schema', 111 | ]; 112 | if (window.customElements) { 113 | i = document.createElement('canvas-datagrid'); 114 | Object.keys(args).forEach(function (argKey) { 115 | // set data and parentNode after everything else 116 | if (argKey === 'data') { 117 | return; 118 | } 119 | if (argKey === 'parentNode') { 120 | return; 121 | } 122 | // top level keys in args 123 | if (tKeys.indexOf(argKey) !== -1) { 124 | tKeys.forEach(function (tKey) { 125 | if (args[tKey] === undefined || tKey !== argKey) { 126 | return; 127 | } 128 | if (['formatters', 'sorters', 'filters'].indexOf(argKey) !== -1) { 129 | if (typeof args[tKey] === 'object' && args[tKey] !== null) { 130 | Object.keys(args[tKey]).forEach(function (sKey) { 131 | i[tKey][sKey] = args[tKey][sKey]; 132 | }); 133 | } 134 | } else { 135 | i[tKey] = args[tKey]; 136 | } 137 | }); 138 | return; 139 | } 140 | // all others are attribute level keys 141 | i.attributes[argKey] = args[argKey]; 142 | }); 143 | if (args.data) { 144 | i.data = args.data; 145 | } 146 | // add to the dom very last to avoid redraws 147 | if (args.parentNode) { 148 | args.parentNode.appendChild(i); 149 | } 150 | return i; 151 | } 152 | args.component = false; 153 | i = new Grid(args); 154 | if (args.parentNode && args.parentNode.appendChild) { 155 | args.parentNode.appendChild(i); 156 | } 157 | return i; 158 | } 159 | -------------------------------------------------------------------------------- /lib/selections/type.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The descriptor for a cells range. 3 | * For example: `{startRow: 1, startCol: 1, endRow: 2, endColumn: 2}` is a range with four cells. 4 | * And `{startRow: 1, endRow: 2}` is a range with two rows. 5 | * - For the cells block, four properties are required. 6 | * - For the rows, `startRow` and `endRow` are required. 7 | * - For the columns, `startCol` and `endColumn` are required. 8 | */ 9 | type RangeDescriptor = { 10 | startRow?: number; 11 | startColumn?: number; 12 | endRow?: number; 13 | endColumn?: number; 14 | }; 15 | 16 | /** 17 | * The descriptor for a selection 18 | * @see SelectionType 19 | */ 20 | type SelectionDescriptor = { 21 | type: number; 22 | } & RangeDescriptor; 23 | 24 | type ContextForSelectionAction = { 25 | rows?: number; 26 | columns?: number; 27 | }; 28 | 29 | type RectangleObject = { 30 | top: number; 31 | bottom: number; 32 | left: number; 33 | right: number; 34 | }; 35 | 36 | type ClipboardInterface = { 37 | setData: (mimeType: string, data: any) => any; 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canvas-datagrid", 3 | "version": "0.4.7", 4 | "description": "Canvas based data grid web component. Capable of displaying millions of contiguous hierarchical rows and columns without paging or loading, on a single canvas element.", 5 | "main": "./dist/canvas-datagrid.js", 6 | "module": "./dist/canvas-datagrid.module.js", 7 | "scripts": { 8 | "build": "rollup -c && webpack", 9 | "postbuild": "npm run build:types", 10 | "prebuild:docs": "node docs/jsdoc-to-md.js", 11 | "build:docs": "cd docs && npm run build", 12 | "format": "prettier lib/**/*.js --write", 13 | "lint": "eslint lib", 14 | "test": "npm run build && karma start", 15 | "test:watch": "karma start --no-single-run", 16 | "watch": "webpack --watch", 17 | "build:types": "jsdoc -c ./jsdoc.ts.conf.js", 18 | "start": "webpack serve --open" 19 | }, 20 | "types": "./dist/types.d.ts", 21 | "files": [ 22 | "dist/*", 23 | "lib/*", 24 | "LICENSE", 25 | "README.md", 26 | "CHANGELOG.md" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/TonyGermaneri/canvas-datagrid.git" 31 | }, 32 | "keywords": [ 33 | "data", 34 | "datagrid", 35 | "grid", 36 | "data-grid", 37 | "data-table", 38 | "table", 39 | "datatable", 40 | "component" 41 | ], 42 | "browserslist": [ 43 | "defaults", 44 | "not IE 11" 45 | ], 46 | "author": "Tony Germaneri ", 47 | "contributors": [ 48 | { 49 | "url": "https://github.com/TonyGermaneri/canvas-datagrid/graphs/contributors" 50 | } 51 | ], 52 | "maintainers": [ 53 | { 54 | "name": "Tony Germaneri", 55 | "email": "Tony.Germaneri@gmail.com" 56 | } 57 | ], 58 | "license": "BSD-3-Clause", 59 | "bugs": { 60 | "url": "https://github.com/TonyGermaneri/canvas-datagrid/issues" 61 | }, 62 | "homepage": "https://canvas-datagrid.js.org/", 63 | "husky": { 64 | "hooks": { 65 | "pre-commit": "lint-staged" 66 | } 67 | }, 68 | "devDependencies": { 69 | "@babel/core": "^7.12.9", 70 | "@babel/preset-env": "^7.12.7", 71 | "@rollup/plugin-babel": "^5.2.1", 72 | "@rollup/plugin-commonjs": "^16.0.0", 73 | "@rollup/plugin-node-resolve": "^10.0.0", 74 | "@rollup/plugin-replace": "^2.3.4", 75 | "babel-loader": "^8.2.1", 76 | "chai": "^4.2.0", 77 | "eslint": "^7.14.0", 78 | "eslint-config-prettier": "^6.15.0", 79 | "eslint-plugin-prettier": "^3.1.4", 80 | "husky": "^8.0.3", 81 | "istanbul": "^0.4.5", 82 | "jsdoc": "^3.6.6", 83 | "karma": "^6.3.14", 84 | "karma-chai": "^0.1.0", 85 | "karma-chrome-launcher": "^3.1.0", 86 | "karma-coverage": "^2.0.2", 87 | "karma-coverage-istanbul-reporter": "^3.0.2", 88 | "karma-mocha": "^2.0.1", 89 | "lint-staged": "^13.2.1", 90 | "marked": "^1.2.5", 91 | "mocha": "^8.2.1", 92 | "prettier": "^2.2.0", 93 | "rollup": "^2.33.3", 94 | "rollup-plugin-clear": "^2.0.7", 95 | "rollup-plugin-terser": "^7.0.2", 96 | "tsd-jsdoc": "^2.5.0", 97 | "webpack": "^5.6.0", 98 | "webpack-cli": "^4.2.0", 99 | "webpack-dev-server": "^3.11.1" 100 | }, 101 | "dependencies": { 102 | "is-printable-key-event": "1.0.0" 103 | }, 104 | "exports": { 105 | "module": "./dist/canvas-datagrid.module.js", 106 | "deno": "./dist/canvas-datagrid.module.js", 107 | "node": { 108 | "import": "./lib/main.js" 109 | }, 110 | "import": "./dist/canvas-datagrid.module.js", 111 | "development": { 112 | "script": "./dist/canvas-datagrid.debug.js", 113 | "require": "./dist/canvas-datagrid.debug.js" 114 | }, 115 | "script": "./dist/canvas-datagrid.js", 116 | "require": "./dist/canvas-datagrid.js", 117 | "default": "./dist/canvas-datagrid.js" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { babel } from '@rollup/plugin-babel'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | import commonjs from '@rollup/plugin-commonjs'; 5 | import replace from '@rollup/plugin-replace'; 6 | import clear from 'rollup-plugin-clear'; 7 | 8 | const input = 'lib/main.js'; 9 | const fileName = 'canvas-datagrid'; 10 | 11 | const babelInput = { 12 | babelHelpers: 'bundled', 13 | presets: [ 14 | [ 15 | '@babel/preset-env', 16 | { 17 | corejs: 3, 18 | useBuiltIns: 'entry', 19 | modules: false, 20 | spec: true, 21 | targets: '> 0.25%, not dead', 22 | }, 23 | ], 24 | ], 25 | }; 26 | export default { 27 | input, 28 | plugins: [ 29 | clear({ targets: ['dist'] }), 30 | replace({ 31 | 'window.EXCLUDE_GLOBAL': 'true', 32 | }), 33 | nodeResolve(), 34 | commonjs(), 35 | babel(babelInput), 36 | ], 37 | output: { 38 | file: `dist/${fileName}.module.js`, 39 | plugins: [terser()], 40 | sourcemap: true, 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /test/data-interface.js: -------------------------------------------------------------------------------- 1 | import { g, assertIf } from './util.js'; 2 | 3 | export default function () { 4 | it('Pass array of objects.', function (done) { 5 | var grid = g({ 6 | test: this.test, 7 | }); 8 | grid.data = [ 9 | { a: 0, b: 1, c: 2 }, 10 | { a: 4, b: 5, c: 6 }, 11 | { a: 7, b: 8, c: 9 }, 12 | ]; 13 | done( 14 | assertIf( 15 | grid.viewData[2].c !== 9, 16 | 'Expected grid to be able to import and export this format', 17 | ), 18 | ); 19 | }); 20 | it('Pass array that contain other arrays of objects.', function (done) { 21 | var grid = g({ 22 | test: this.test, 23 | }); 24 | grid.data = [ 25 | { a: 0, b: 1, c: 2 }, 26 | { 27 | a: 4, 28 | b: [ 29 | { a: 0, b: 1, c: 2 }, 30 | { a: 4, b: 5, c: 6 }, 31 | { a: 7, b: 8, c: 9 }, 32 | ], 33 | c: 6, 34 | }, 35 | { a: 7, b: 8, c: 9 }, 36 | ]; 37 | //TODO: this test cannot work until cell grids are fixed https://github.com/TonyGermaneri/canvas-datagrid/issues/35 38 | // so this test success is false 39 | done(); 40 | }); 41 | it('Pass array that contains an array of objects with mixed object/primitives as values.', function (done) { 42 | var grid = g({ 43 | test: this.test, 44 | }); 45 | grid.data = [ 46 | { a: 0, b: 1, c: 2 }, 47 | { a: 4, b: { a: 0, b: 1, c: 2 }, c: 6 }, 48 | { a: 7, b: 8, c: 9 }, 49 | ]; 50 | //TODO: this test cannot work until cell grids are fixed https://github.com/TonyGermaneri/canvas-datagrid/issues/35 51 | // so this test success is false 52 | done(); 53 | }); 54 | it('Pass jagged data', function (done) { 55 | var grid = g({ 56 | test: this.test, 57 | }); 58 | grid.data = [['a', 'b', 'c'], ['1', '2'], ['q']]; 59 | done( 60 | assertIf( 61 | grid.viewData[0][0] !== 'a', 62 | 'Expected grid to be able to import and export this format', 63 | ), 64 | ); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /test/drawing.js: -------------------------------------------------------------------------------- 1 | import { 2 | mouseup, 3 | mousedown, 4 | mousemove, 5 | assertPxColor, 6 | g, 7 | smallData, 8 | c, 9 | } from './util.js'; 10 | 11 | export default function () { 12 | it('Should draw row selections.', function (done) { 13 | var grid = g({ 14 | test: this.test, 15 | data: smallData(), 16 | selectionMode: 'row', 17 | style: { 18 | activeCellSelectedBackgroundColor: c.b, 19 | cellSelectedBackgroundColor: c.b, 20 | }, 21 | }); 22 | 23 | mousemove(window, 45, 37, grid.canvas); 24 | mousedown(grid.canvas, 45, 37); 25 | mouseup(grid.canvas, 45, 37); 26 | mousemove(window, 45, 37, grid.canvas); 27 | setTimeout(function () { 28 | assertPxColor(grid, 80, 37, c.b, done); 29 | }, 1); 30 | }); 31 | it('Should draw a debug message.', function (done) { 32 | var grid = g({ 33 | test: this.test, 34 | data: smallData(), 35 | debug: true, 36 | style: { 37 | debugFont: '200px sans-serif', 38 | debugColor: c.b, 39 | }, 40 | }); 41 | mousemove(grid.canvas, 100, 113); 42 | assertPxColor(grid, 90, 10, c.b, done); 43 | }); 44 | // phantom throws a nonsense error due to the way the data url is constructed in the html function 45 | it('Should draw HTML.', function (done) { 46 | var grid = g({ 47 | test: this.test, 48 | data: [ 49 | { 50 | a: 51 | 'blah', 56 | }, 57 | ], 58 | schema: [{ name: 'a', type: 'html' }], 59 | }); 60 | setTimeout(function () { 61 | assertPxColor(grid, 50, 32, c.b, done); 62 | }, 100); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /test/filters.js: -------------------------------------------------------------------------------- 1 | import { g, assertIf, doAssert } from './util.js'; 2 | 3 | export default function () { 4 | it('Should filter for given value', function (done) { 5 | var grid = g({ 6 | test: this.test, 7 | data: [{ d: 'abcd' }, { d: 'edfg' }], 8 | }); 9 | grid.setFilter('d', 'edfg'); 10 | done( 11 | assertIf( 12 | grid.viewData.length === 0 && grid.viewData[0].d === 'edfg', 13 | 'Expected filter to remove all but 1 row.', 14 | ), 15 | ); 16 | }); 17 | it('Should filter for blank values', function (done) { 18 | var grid = g({ 19 | test: this.test, 20 | data: [ 21 | { d: 'abcd' }, 22 | { d: null }, 23 | { d: undefined }, 24 | { d: '' }, 25 | { d: ' ' }, 26 | { d: 'edfg' }, 27 | ], 28 | }); 29 | grid.setFilter('d', '(Blanks)'); 30 | var filteredValuesOnly = grid.viewData.map((obj) => obj.d); 31 | var onlyBlanks = 32 | filteredValuesOnly.length === 4 && 33 | filteredValuesOnly.every((item) => 34 | [undefined, null, '', ' '].includes(item), 35 | ); 36 | done(assertIf(!onlyBlanks, 'Expected filter remove non-null/empty values')); 37 | }); 38 | it('Should filter for blank values (numbers)', function (done) { 39 | var grid = g({ 40 | test: this.test, 41 | data: [{ d: 1 }, { d: null }, { d: undefined }, { d: '' }, { d: 2 }], 42 | schema: [{ name: 'd', type: 'number' }], 43 | }); 44 | grid.setFilter('d', '(Blanks)'); 45 | var filteredValuesOnly = grid.viewData.map((obj) => obj.d); 46 | var onlyBlanks = 47 | filteredValuesOnly.length === 3 && 48 | filteredValuesOnly.every((item) => [undefined, null, ''].includes(item)); 49 | done(assertIf(!onlyBlanks, 'Expected filter remove non-null/empty values')); 50 | }); 51 | it('Should remove all filters', function (done) { 52 | var grid = g({ 53 | test: this.test, 54 | data: [ 55 | { d: 'abcd', e: 'qwert' }, 56 | { d: 'edfg', e: 'asdfg' }, 57 | ], 58 | }); 59 | grid.setFilter('d', 'edfg'); 60 | grid.setFilter('e', 'asdfg'); 61 | grid.setFilter(); 62 | done( 63 | assertIf( 64 | grid.viewData.length !== 2, 65 | 'Expected to see all the records return.', 66 | ), 67 | ); 68 | }); 69 | it('Should remove a specific filter by passing empty string', function (done) { 70 | var grid = g({ 71 | test: this.test, 72 | data: [ 73 | { d: 'abcd', e: 'qwert' }, 74 | { d: 'edfg', e: 'asdfg' }, 75 | ], 76 | }); 77 | grid.setFilter('d', 'edfg'); 78 | grid.setFilter('e', 'asdfg'); 79 | grid.setFilter('e', ''); 80 | done( 81 | assertIf(grid.viewData.length !== 1, 'Expected to see 1 of the records.'), 82 | ); 83 | }); 84 | it('Should remove a specific filter by passing undefined', function (done) { 85 | var grid = g({ 86 | test: this.test, 87 | data: [ 88 | { d: 'abcd', e: 'qwert' }, 89 | { d: 'edfg', e: 'asdfg' }, 90 | ], 91 | }); 92 | grid.setFilter('d', 'edfg'); 93 | grid.setFilter('e', 'asdfg'); 94 | grid.setFilter('e'); 95 | done( 96 | assertIf(grid.viewData.length !== 1, 'Expected to see 1 of the records.'), 97 | ); 98 | }); 99 | it('Should use RegExp as a filter', function (done) { 100 | var grid = g({ 101 | test: this.test, 102 | data: [{ d: 'abcd' }, { d: 'edfg' }], 103 | }); 104 | grid.setFilter('d', '/\\w/'); 105 | done( 106 | assertIf( 107 | grid.viewData.length === 0 && grid.viewData[0].d === 'edfg', 108 | 'Expected to see a row after a RegExp value.', 109 | ), 110 | ); 111 | }); 112 | it('Should tolerate RegExp errors', function (done) { 113 | var grid = g({ 114 | test: this.test, 115 | data: [{ d: 'abcd' }, { d: 'edfg' }], 116 | }); 117 | grid.setFilter('d', '/{1}/'); 118 | done(); 119 | }); 120 | it('Should not reset filter when non-existant columns passed to setFilter', function (done) { 121 | var grid = g({ 122 | test: this.test, 123 | data: [{ d: 'abcd' }, { d: 'edfg' }], 124 | }); 125 | grid.setFilter('d', 'a'); 126 | grid.setFilter('x', 'a'); 127 | done( 128 | assertIf(grid.viewData.length !== 1, 'Expected to see only 1 record.'), 129 | ); 130 | }); 131 | it('Should apply correct type filtering method when column filter not set', function (done) { 132 | var grid = g({ 133 | test: this.test, 134 | data: [{ num: 1234 }, { num: 1 }], 135 | schema: [{ name: 'num', type: 'int' }], 136 | filters: { 137 | int: function (value, filterFor) { 138 | return !filterFor || value.toString() === filterFor; 139 | }, 140 | }, 141 | }); 142 | delete grid.schema[0].filter; 143 | grid.setFilter('num', '1'); 144 | done( 145 | assertIf(grid.viewData.length !== 1, 'Expected to see only 1 record.'), 146 | ); 147 | }); 148 | it('Should apply filter to new data when data is set', function (done) { 149 | var grid = g({ 150 | test: this.test, 151 | data: [{ d: 'abcd' }, { d: 'edfg' }], 152 | }); 153 | grid.setFilter('d', 'a'); 154 | grid.data = [{ d: 'gfde' }, { d: 'dcba' }]; 155 | done( 156 | assertIf(grid.viewData.length !== 1, 'Expected to see only 1 record.'), 157 | ); 158 | }); 159 | it('Should retain filters of columns not in new data when data is set', function (done) { 160 | var grid = g({ 161 | test: this.test, 162 | data: [{ d: 'abcd' }, { d: 'edfg' }], 163 | }); 164 | grid.setFilter('d', 'a'); 165 | 166 | grid.data = [{ x: 'aaaa' }, { x: 'aaaa' }]; 167 | grid.data = [{ d: 'gfde' }, { d: 'dcba' }]; 168 | 169 | done( 170 | assertIf(grid.viewData.length !== 1, 'Expected to see only 1 record.'), 171 | ); 172 | }); 173 | it('filter ignores row on and above frozen row', function (done) { 174 | var grid = g({ 175 | test: this.test, 176 | data: [ 177 | { d: 'baz' }, 178 | { d: 'foo frozen row' }, 179 | { d: 'foo1' }, 180 | { d: 'foo2' }, 181 | { d: 'bar' }, 182 | ], 183 | allowFreezingRows: true, 184 | filterFrozenRows: false, 185 | frozenRow: 1, 186 | }); 187 | grid.frozenRow = 2; 188 | grid.setFilter('d', 'bar'); 189 | 190 | done( 191 | doAssert( 192 | grid.viewData.length === 3 && 193 | grid.viewData[0].d === 'baz' && 194 | grid.viewData[1].d === 'foo frozen row' && 195 | grid.viewData[2].d === 'bar', 196 | 'Expected filter to filter ignore rows on and above frozen row.', 197 | ), 198 | ); 199 | }); 200 | } 201 | -------------------------------------------------------------------------------- /test/formatters.js: -------------------------------------------------------------------------------- 1 | import { assertPxColor, blocks, g, c } from './util.js'; 2 | 3 | export default function () { 4 | it('Should format values using formating functions', function (done) { 5 | var grid = g({ 6 | test: this.test, 7 | data: [{ d: '' }], 8 | schema: [{ name: 'd', type: 's' }], 9 | formatters: { 10 | s: function () { 11 | return blocks; 12 | }, 13 | }, 14 | }); 15 | assertPxColor(grid, 90, 32, c.black, done); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mocha Tests 7 | 8 | 9 | 26 | 27 | 28 | 29 | 33 | 34 | 42 | Latest 43 | Coverage Report 44 |
45 |
46 |
47 |
48 | 106 | 107 | %SCRIPTS% 108 | 110 | 116 | 119 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*jslint browser: true*/ 4 | /*globals Event: false, describe: false, afterEach: false, beforeEach: false, after: false, it: false, canvasDatagrid: false, async: false, requestAnimationFrame: false*/ 5 | 6 | import { cleanup, mouseup, click } from './util.js'; 7 | 8 | import instantationTests from './instantation.js'; 9 | import drawingTests from './drawing.js'; 10 | import styleTests from './style.js'; 11 | import dataInterfaceTests from './data-interface.js'; 12 | import touchTests from './touch.js'; 13 | import editingTests from './editing.js'; 14 | import keyNavigationTests from './key-navigation.js'; 15 | import resizeTests from './resize.js'; 16 | import formattersTests from './formatters.js'; 17 | import sortersTests from './sorters.js'; 18 | import selectionsTests from './selections.js'; 19 | import filtersTests from './filters.js'; 20 | import attributesTests from './attributes.js'; 21 | import groupsTests from './groups.js'; 22 | import reorderColumnsTests from './reorder-columns.js'; 23 | import publicInterfaceTests from './public-interface.js'; 24 | import contextMenuTests from './context-menu.js'; 25 | import webComponentTests from './web-component.js'; 26 | import scrollingTests from './scrolling.js'; 27 | import unhideIndicatorTests from './unhide-indicator.js'; 28 | 29 | import unitTests from './unit/index.js'; 30 | 31 | describe('canvas-datagrid', function () { 32 | after(function (done) { 33 | // git rid of lingering artifacts from the run 34 | mouseup(document.body, 1, 1); 35 | mouseup(document.body, 1, 1); 36 | click(document.body, 1, 1); 37 | done(); 38 | }); 39 | beforeEach(cleanup); 40 | afterEach(cleanup); 41 | describe('Integration Tests', function () { 42 | describe('Instantiation', instantationTests); 43 | describe('Web component', webComponentTests); 44 | describe('Drawing', drawingTests); 45 | describe('Styles', styleTests); 46 | describe('Data interface', dataInterfaceTests); 47 | describe('Public interface', publicInterfaceTests); 48 | describe('Context menu', contextMenuTests); 49 | describe('Scroll box with scrollPointerLock false', scrollingTests); 50 | describe('Touch', touchTests); 51 | describe('Editing', editingTests); 52 | describe('Key navigation', keyNavigationTests); 53 | describe('Resize', resizeTests); 54 | describe('Formatters', formattersTests); 55 | describe('Sorters', sortersTests); 56 | describe('Selections', selectionsTests); 57 | describe('Filters', filtersTests); 58 | describe('Attributes', attributesTests); 59 | describe('Groups', groupsTests); 60 | describe('Unhide indicator', unhideIndicatorTests); 61 | describe('Reorder columns', reorderColumnsTests); 62 | }); 63 | describe('Unit Tests', unitTests); 64 | }); 65 | -------------------------------------------------------------------------------- /test/instantation.js: -------------------------------------------------------------------------------- 1 | import { assertPxColor, g, smallData, assertIf, c } from './util.js'; 2 | 3 | export default function () { 4 | it('Should be callable without arguments.', function (done) { 5 | canvasDatagrid(); 6 | done(); 7 | }); 8 | it('Should create an instance of datagrid', function (done) { 9 | var grid = g({ test: this.test }); 10 | assertIf(!grid, 'Expected a grid instance, instead got something false'); 11 | grid.style.gridBackgroundColor = c.y; 12 | assertPxColor(grid, 80, 32, c.y, done); 13 | }); 14 | it('Should create, then completely annihilate the grid.', function (done) { 15 | var grid = g({ test: this.test }); 16 | grid.dispose(); 17 | done( 18 | assertIf(!grid.parentNode, 'Expected to see the grid gone, it is not.'), 19 | ); 20 | }); 21 | it('Should create a grid and set data, data should be visible.', function (done) { 22 | var grid = g({ 23 | test: this.test, 24 | data: smallData(), 25 | }); 26 | grid.style.activeCellBackgroundColor = c.b; 27 | assertIf( 28 | grid.viewData.length !== 3, 29 | 'Expected to see data in the interface.', 30 | ); 31 | assertPxColor(grid, 80, 32, c.b, done); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/reorder-columns.js: -------------------------------------------------------------------------------- 1 | import { mouseup, mousedown, mousemove, contextmenu, g, marker, click, dblclick } from './util.js'; 2 | 3 | export default function () { 4 | it('Hide columns by context menu item after reordering', async function () { 5 | const baseWidth = 60; 6 | const halfBaseWidth = baseWidth * 0.5; 7 | const data = [{ c1: 'c1', c2: 'c2', c3: 'c3', c4: 'c4', c5: 'c5' }]; 8 | const schema = Object.keys(data[0]).map((name) => ({ name, width: baseWidth })); 9 | for (let i = 0; i < 3; i++) data.push(Object.assign({}, data[0])); 10 | 11 | const test = this.test; 12 | const grid = g({ 13 | test: test, 14 | schema, 15 | data, 16 | allowRowReordering: true, 17 | showFilter: false, 18 | }); 19 | const headerWidth = grid.sizes.columns[-1] || baseWidth; 20 | 21 | const dnd = (x1, y1, x2, y2) => { 22 | mousemove(window, x1, y1, grid.canvas); 23 | mousedown(grid.canvas, x1, y1); 24 | mousemove(window, x2, y2, grid.canvas); 25 | mouseup(window, x2, y2, grid.canvas); 26 | }; 27 | let contextMenuItems = []; 28 | grid.addEventListener('contextmenu', function (e) { 29 | contextMenuItems = e.items; 30 | }); 31 | 32 | grid.focus(); 33 | // c1, c2, c3, c4, c5 => c2, c1, c3, c4, c5 34 | dnd(headerWidth + halfBaseWidth, 10, headerWidth + baseWidth + halfBaseWidth, 10); 35 | await delay(); 36 | 37 | grid.selectColumn(0); 38 | grid.draw(); 39 | await delay(); 40 | contextmenu(grid.canvas, headerWidth + halfBaseWidth, 10); 41 | await delay(); 42 | 43 | const hideColumnItem = contextMenuItems.find((it) => 44 | it.title.startsWith('Hide '), 45 | ); 46 | if (!hideColumnItem) throw new Error(`No menu item for hidding column`); 47 | hideColumnItem.click(new Event('keyup')); 48 | await delay(30); 49 | 50 | const cells = getVisibleCells(grid); 51 | cells.forEach((it) => { 52 | if (it.header.name === 'c2') 53 | throw new Error(`There is column c2 after hidding first column`); 54 | }); 55 | }); 56 | } 57 | 58 | function delay(ms = 1) { 59 | return new Promise((resolve) => setTimeout(resolve, ms)) 60 | } 61 | 62 | /** @returns {any[]} */ 63 | function getVisibleCells(grid) { 64 | return grid.visibleCells.filter( 65 | (it) => it.style === 'cell' || it.style === 'activeCell', 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /test/style.js: -------------------------------------------------------------------------------- 1 | import { assertPxColor, g, smallData, c } from './util.js'; 2 | 3 | export default function () { 4 | it('Should set the active cell color to black.', function (done) { 5 | var grid = g({ 6 | test: this.test, 7 | data: smallData(), 8 | }); 9 | grid.style.activeCellBackgroundColor = c.black; 10 | assertPxColor(grid, 100, 32, c.black, done); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /test/touch.js: -------------------------------------------------------------------------------- 1 | import { 2 | makeData, 3 | touchend, 4 | touchmove, 5 | touchstart, 6 | g, 7 | smallData, 8 | assertIf, 9 | } from './util.js'; 10 | 11 | export default function () { 12 | it.skip('Touch and drag should scroll the grid vertically and horizontally', function (done) { 13 | var grid = g({ 14 | test: this.test, 15 | data: smallData(), 16 | }); 17 | setTimeout(function () { 18 | grid.focus(); 19 | touchstart(grid.canvas, 200, 37); 20 | touchmove(document.body, 90, 37, grid.canvas); 21 | setTimeout(function () { 22 | // simulate very slow movement of humans 23 | touchmove(document.body, 60, 66, grid.canvas); 24 | touchend(document.body, 60, 66, grid.canvas); 25 | setTimeout(function () { 26 | done( 27 | assertIf( 28 | grid.scrollLeft === 0, 29 | 'Expected the grid to scroll some.', 30 | ), 31 | ); 32 | }, 1500); 33 | }, 200); 34 | }, 1); 35 | }); 36 | it.skip('Touch and drag should scroll the inner grid', function (done) { 37 | var grid = g({ 38 | test: this.test, 39 | data: smallData(), 40 | tree: true, 41 | }); 42 | grid.addEventListener('expandtree', function (e) { 43 | setTimeout(function () { 44 | e.treeGrid.focus(); 45 | touchstart(grid.canvas, 200, 80); 46 | touchmove(document.body, 90, 80, grid.canvas); 47 | setTimeout(function () { 48 | // simulate very slow movement of humans 49 | touchmove(document.body, 60, 80, grid.canvas); 50 | touchend(document.body, 60, 80, grid.canvas); 51 | setTimeout(function () { 52 | done( 53 | assertIf( 54 | e.treeGrid.scrollLeft === 0, 55 | 'Expected the grid to scroll some.', 56 | ), 57 | ); 58 | }, 1500); 59 | }, 200); 60 | }, 1); 61 | e.treeGrid.data = [{ a: 'b', c: 'd', e: 'f', g: 'h' }]; 62 | }); 63 | grid.expandTree(0); 64 | }); 65 | it.skip('Touch and drag on the scroll bar should engage fast scrolling', function (done) { 66 | var grid = g({ 67 | test: this.test, 68 | data: makeData(30, 500), 69 | }); 70 | setTimeout(function () { 71 | grid.focus(); 72 | touchstart(grid.canvas, 50, 113); 73 | touchmove(document.body, 50, 113, grid.canvas); 74 | setTimeout(function () { 75 | // simulate very slow movement of humans 76 | touchmove(document.body, 100, 113, grid.canvas); 77 | touchend(document.body, 100, 113, grid.canvas); 78 | setTimeout(function () { 79 | done( 80 | assertIf( 81 | grid.scrollLeft < 400, 82 | 'Expected the scroll bar to be further along.', 83 | ), 84 | ); 85 | }, 100); 86 | }, 200); 87 | }, 1); 88 | }); 89 | it('Use touchstart event to prevent touch events using e.preventDefault.', function (done) { 90 | var grid = g({ 91 | test: this.test, 92 | data: smallData(), 93 | }); 94 | grid.addEventListener('touchstart', function (e) { 95 | return e.preventDefault(); 96 | }); 97 | setTimeout(function () { 98 | grid.focus(); 99 | touchstart(grid.canvas, 200, 37); 100 | setTimeout(function () { 101 | // simulate very slow movement of humans 102 | grid.focus(); 103 | touchmove(document.body, 320, 90, grid.canvas); 104 | touchend(document.body, 320, 90, grid.canvas); 105 | setTimeout(function () { 106 | done(assertIf(grid.scrollLeft !== 0, 'Expected no movement.')); 107 | }, 1); 108 | }, 1000); 109 | }, 1); 110 | }); 111 | it.skip('Use touchend event to prevent touch events using e.preventDefault.', function (done) { 112 | var grid = g({ 113 | test: this.test, 114 | data: smallData(), 115 | }); 116 | grid.addEventListener('touchend', function (e) { 117 | return e.preventDefault(); 118 | }); 119 | setTimeout(function () { 120 | grid.focus(); 121 | touchstart(grid.canvas, 200, 37); 122 | setTimeout(function () { 123 | // simulate very slow movement of humans 124 | grid.focus(); 125 | touchmove(document.body, 320, 90, grid.canvas); 126 | touchend(document.body, 320, 90, grid.canvas); 127 | setTimeout(function () { 128 | done(assertIf(grid.scrollLeft !== 0, 'Expected no movement.')); 129 | }, 1); 130 | }, 1000); 131 | }, 1); 132 | }); 133 | it.skip('Touch and hold should not start selecting or moving if very little movement before touchEnd', function (done) { 134 | var grid = g({ 135 | test: this.test, 136 | data: smallData(), 137 | }); 138 | setTimeout(function () { 139 | grid.focus(); 140 | touchstart(grid.canvas, 200, 37); 141 | setTimeout(function () { 142 | // simulate very slow movement of humans 143 | grid.focus(); 144 | touchmove(document.body, 200, 38, grid.canvas); 145 | touchend(document.body, 200, 38, grid.canvas); 146 | setTimeout(function () { 147 | done(assertIf(grid.scrollLeft !== 0, 'Expected no movement.')); 148 | }, 1); 149 | }, 1000); 150 | }, 1); 151 | }); 152 | } 153 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import parseClipboardData from './parse-clip-board-data.js'; 4 | import mergeHiddenRowRanges from './merge-hidden-row-ranges.js'; 5 | import selectionTests from './selections.js'; 6 | 7 | export default function () { 8 | describe('clipboard', parseClipboardData); 9 | describe('mergeHiddenRowRanges', mergeHiddenRowRanges); 10 | describe('selections', selectionTests); 11 | } 12 | -------------------------------------------------------------------------------- /test/unit/merge-hidden-row-ranges.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { mergeHiddenRowRanges } from '../../lib/groups/util.js'; 4 | 5 | export default function () { 6 | const { deepStrictEqual } = chai.assert; 7 | it('invalid ranges can not be merged', function () { 8 | const ranges = []; 9 | deepStrictEqual(mergeHiddenRowRanges(ranges, [10, 5]), false); 10 | deepStrictEqual(ranges, []); 11 | }); 12 | 13 | it('merge range only contains one row', function () { 14 | const ranges = []; 15 | deepStrictEqual(mergeHiddenRowRanges(ranges, [1, 1]), true); 16 | deepStrictEqual(ranges, [[1, 1]]); 17 | 18 | deepStrictEqual(mergeHiddenRowRanges(ranges, [2, 2]), true); 19 | deepStrictEqual(ranges, [[1, 2]]); 20 | 21 | deepStrictEqual(mergeHiddenRowRanges(ranges, [5, 5]), true); 22 | deepStrictEqual(ranges, [ 23 | [1, 2], 24 | [5, 5], 25 | ]); 26 | 27 | deepStrictEqual(mergeHiddenRowRanges(ranges, [1, 5]), true); 28 | deepStrictEqual(ranges, [[1, 5]]); 29 | }); 30 | 31 | it('merge ranges', function () { 32 | const ranges = []; 33 | deepStrictEqual(mergeHiddenRowRanges(ranges, [5, 10]), true); 34 | deepStrictEqual(ranges, [[5, 10]]); 35 | 36 | deepStrictEqual(mergeHiddenRowRanges(ranges, [5, 11]), true); 37 | deepStrictEqual(ranges, [[5, 11]]); 38 | 39 | deepStrictEqual(mergeHiddenRowRanges(ranges, [12, 15]), true); 40 | deepStrictEqual(ranges, [[5, 15]]); 41 | }); 42 | 43 | it('merge independent intervals', function () { 44 | const ranges = [[5, 15]]; 45 | deepStrictEqual(mergeHiddenRowRanges(ranges, [20, 30]), true); 46 | deepStrictEqual(ranges, [ 47 | [5, 15], 48 | [20, 30], 49 | ]); 50 | 51 | deepStrictEqual(mergeHiddenRowRanges(ranges, [1, 3]), true); 52 | deepStrictEqual(ranges, [ 53 | [1, 3], 54 | [5, 15], 55 | [20, 30], 56 | ]); 57 | }); 58 | 59 | it('new range spans two existed ranges', function () { 60 | let ranges = [ 61 | [1, 3], 62 | [5, 15], 63 | [20, 30], 64 | ]; 65 | deepStrictEqual(mergeHiddenRowRanges(ranges, [7, 25]), true); 66 | deepStrictEqual(ranges, [ 67 | [1, 3], 68 | [5, 30], 69 | ]); 70 | 71 | ranges = [ 72 | [1, 3], 73 | [5, 15], 74 | [20, 30], 75 | ]; 76 | deepStrictEqual(mergeHiddenRowRanges(ranges, [7, 35]), true); 77 | deepStrictEqual(ranges, [ 78 | [1, 3], 79 | [5, 35], 80 | ]); 81 | }); 82 | 83 | it('new range wraps all existed ranges', function () { 84 | const ranges = [ 85 | [1, 3], 86 | [5, 15], 87 | [20, 30], 88 | ]; 89 | deepStrictEqual(mergeHiddenRowRanges(ranges, [1, 50]), true); 90 | deepStrictEqual(ranges, [[1, 50]]); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /test/unit/parse-clip-board-data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | parseHtmlTable, 5 | parseHtmlText, 6 | parseText, 7 | } from '../../lib/events/util.js'; 8 | 9 | export default function () { 10 | it('parse html table', function () { 11 | const htmlTable = ` 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
First cellSecond cell
23 | 24 | `; 25 | 26 | const result = parseHtmlTable(htmlTable); 27 | 28 | chai.assert.deepStrictEqual(result, [ 29 | [ 30 | { value: [{ value: 'First cell' }] }, 31 | { value: [{ value: 'Second cell' }] }, 32 | ], 33 | ]); 34 | // doAssert( 35 | // JSON.stringify(result) === expectedResultString, 36 | // 'get expected html table values', 37 | // ); 38 | }); 39 | 40 | it('parse html text', function () { 41 | const htmlTable = ` 42 |
43 |
44 | Paste value 45 |
46 |
`; 47 | 48 | const result = parseHtmlText(htmlTable); 49 | 50 | chai.assert.deepStrictEqual(result, [ 51 | [{ value: [{ value: 'Paste value' }] }], 52 | ]); 53 | }); 54 | 55 | it('parse plain text', function () { 56 | const result = parseText('Single value'); 57 | 58 | chai.assert.deepStrictEqual(result, [ 59 | [{ value: [{ value: 'Single value' }] }], 60 | ]); 61 | }); 62 | 63 | it('parse mulitline plain text', function () { 64 | const result = parseText('First value\nSecond value'); 65 | 66 | chai.assert.deepStrictEqual(result, [ 67 | [{ value: [{ value: 'First value' }] }], 68 | [{ value: [{ value: 'Second value' }] }], 69 | ]); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /test/web-component.js: -------------------------------------------------------------------------------- 1 | import { assertPxColor, blocks, g, smallData, assertIf, c } from './util.js'; 2 | 3 | export default function () { 4 | if (!window.customElements) return; 5 | 6 | it('Should create a web component, set a style', function (done) { 7 | var grid = g({ 8 | test: this.test, 9 | data: smallData(), 10 | component: true, 11 | }); 12 | grid.style.activeCellBackgroundColor = c.b; 13 | assertIf( 14 | grid.viewData.length !== 3, 15 | 'Expected to see data in the interface.', 16 | ); 17 | assertPxColor(grid, 80, 32, c.b, done); 18 | }); 19 | it('Should create a web component and set a hyphenated style', function (done) { 20 | var grid = g({ 21 | test: this.test, 22 | data: smallData(), 23 | component: true, 24 | }); 25 | grid.style['active-cell-background-color'] = c.b; 26 | assertIf( 27 | grid.viewData.length !== 3, 28 | 'Expected to see data in the interface.', 29 | ); 30 | assertPxColor(grid, 80, 32, c.b, done); 31 | }); 32 | it('Should create a web component and set a hyphenated style with a custom prefix', function (done) { 33 | var grid = g({ 34 | test: this.test, 35 | data: smallData(), 36 | component: true, 37 | }); 38 | grid.style['--cdg-active-cell-background-color'] = c.b; 39 | assertIf( 40 | grid.viewData.length !== 3, 41 | 'Expected to see data in the interface.', 42 | ); 43 | assertPxColor(grid, 80, 32, c.b, done); 44 | }); 45 | it('Should create a web component and set a schema', function (done) { 46 | var grid = g({ 47 | test: this.test, 48 | data: [{ a: blocks }], 49 | component: true, 50 | }); 51 | grid.style.gridBackgroundColor = c.b; 52 | grid.schema = [{ name: 'a', width: 30 }]; 53 | assertIf( 54 | grid.viewData.length !== 1, 55 | 'Expected to see data in the interface.', 56 | ); 57 | assertPxColor(grid, 80, 32, c.b, done); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /tutorials/amdDemo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /tutorials/amdDemo.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true */ 2 | require(['../dist/canvas-datagrid.debug.js'], function (dataGrid) { 3 | 'use strict'; 4 | var grid = dataGrid({ 5 | parentNode: document.getElementById('grid') 6 | }); 7 | grid.addEventListener('contextmenu', function (e) { 8 | e.items.push({ 9 | title: 'View page source', 10 | click: function () { window.open('https://github.com/TonyGermaneri/canvas-datagrid/blob/master/tutorials/amdDemo.html', 'src'); } 11 | }); 12 | e.items.push({ 13 | title: 'View JS module', 14 | click: function () { window.open('https://github.com/TonyGermaneri/canvas-datagrid/blob/master/tutorials/amdDemo.js', 'src'); } 15 | }); 16 | e.items.push({ 17 | title: 'Go to main canvas-datagrid GitHub page', 18 | click: function () { window.open('https://github.com/TonyGermaneri/canvas-datagrid', 'src'); } 19 | }); 20 | }); 21 | grid.data = [{a: 0, b: 1, c: 2}]; 22 | }); 23 | -------------------------------------------------------------------------------- /tutorials/canvasDatagrid.contextMenuItem.md: -------------------------------------------------------------------------------- 1 | You can add or remove items from the context menu, or stop it from appearing. 2 | In the following example, a context menu item is added: 3 | 4 | grid.addEventListener('contextmenu', function (e) { 5 | e.items.push({ 6 | title: 'Process selected row(s)', 7 | click: function () { 8 | // e.cell.value contains the cell's value 9 | // e.cell.data contains the row values 10 | myProcess(e.cell.value, e.cell.data); 11 | } 12 | }); 13 | }); 14 | 15 | The `title` property can be an HTML node reference instead of a string. 16 | The `click` property is optional. See [contextmenu](https://tonygermaneri.github.io/canvas-datagrid/#tutorial--Simple-context-menu) complete information. 17 | -------------------------------------------------------------------------------- /tutorials/canvasDatagrid.style.md: -------------------------------------------------------------------------------- 1 | Setting Styles 2 | -------------- 3 | All visual elements of the canvas are dependent on the values of the style object. 4 | Using the style object, you can change the dimensions and appearance of any element of the grid. 5 | 6 | There are two types of styles, styles built into the DOM element, such as width and margin, and there 7 | are styles related to the drawing of the grid on the canvas, these are listed in the style section. 8 | 9 | Styles can be set during instantiation. 10 | 11 | var grid = canvasDatagrid({ 12 | style: { 13 | gridBackgroundColor: 'red' 14 | } 15 | }); 16 | 17 | Styles can be set after instantiation. 18 | 19 | grid.style.gridBackgroundColor = 'red'; 20 | 21 | When using the web component, styles can be set as above, but also using standard CSS. 22 | 23 | When using standard CSS, style names are hyphenated, lower case, and prefixed with `--cdg-`. 24 | 25 | [{"my": "data"}] 26 | 27 | When using the web component you can also use CSS classes and selectors as you would a native HTML element. 28 | 29 | 34 | 35 | [{"my": "data"}] 36 | 37 | You can build your own styles using the Style Builder. 38 | -------------------------------------------------------------------------------- /tutorials/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /tutorials/developer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 54 | 55 | 56 | 57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /tutorials/developer.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function () { 2 | var parentNode = document.body; 3 | // create a new grid 4 | //var grid = document.createElement('canvas-datagrid'); 5 | var grid = canvasDatagrid(); 6 | grid.className = 'myGridStyle'; 7 | grid.data = [ 8 | {col1: 'foo', col2: 0, col3: 'a'}, 9 | {col1: 'bar', col2: 1, col3: 'b'}, 10 | {col1: 'baz', col2: 2, col3: 'c'} 11 | ]; 12 | grid.style.width = '100%'; 13 | grid.style.height = '100%'; 14 | parentNode.appendChild(grid); 15 | }); -------------------------------------------------------------------------------- /tutorials/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | API Documentation - Canvas Datagrid 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tutorials/largeArraysDemo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tutorials/largeArraysDemo.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true*/ 2 | /*globals canvasDatagrid: false*/ 3 | function g() { 4 | 'use strict'; 5 | var x, 6 | grid = canvasDatagrid(), 7 | data = [], 8 | schema = []; 9 | data.length = Math.pow(10, 7); 10 | /// create columns A - Z 11 | for (x = 0; x < 26; x += 1) { 12 | schema.push({ 13 | name: String.fromCharCode(65 + x) 14 | }); 15 | } 16 | // constrain the height/width or we'd get a really large canvas element 17 | document.body.appendChild(grid); 18 | grid.style.height = '100%'; 19 | grid.style.width = '100%'; 20 | grid.schema = schema; 21 | grid.data = data; 22 | } -------------------------------------------------------------------------------- /tutorials/pivotFormDemo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /tutorials/pivotFormDemo.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true*/ 2 | /*globals canvasDatagrid: false*/ 3 | var data; 4 | function demo() { 5 | 'use strict'; 6 | var searchUrl = window.location.search.substring(3), 7 | typeMap = { 8 | 'text': 'string', 9 | 'money': 'number', 10 | 'number': 'number' 11 | }; 12 | function isNoiseData(name) { 13 | // get rid of fields that we don't care about 14 | return ['sid', 'id', 'position', 'created_at', 15 | 'created_meta', 'updated_at', 16 | 'updated_meta', 'meta'].indexOf(name) !== -1; 17 | } 18 | function parseOpenData(openData) { 19 | var data, schema = []; 20 | openData.meta.view.columns.forEach(function (column) { 21 | if (isNoiseData(column.name)) { 22 | column.hidden = true; 23 | } 24 | column.type = typeMap[column.dataTypeName] || 'string'; 25 | if (/full or part-time/i.test(column.name)) { 26 | column.enum = [['F', 'F'], ['P', 'P']]; 27 | } 28 | if (/salary or hourly/i.test(column.name)) { 29 | column.enum = [['Salary', 'Salary'], ['Hourly', 'Hourly']]; 30 | } 31 | schema.push(column); 32 | }); 33 | data = openData.data.map(function (row) { 34 | var r = {}; 35 | schema.forEach(function (column, index) { 36 | r[column.name] = row[index]; 37 | }); 38 | return r; 39 | }); 40 | return { 41 | data: data, 42 | schema: schema 43 | }; 44 | } 45 | function loadDataSet(url) { 46 | var xhr = new XMLHttpRequest(), 47 | grid = canvasDatagrid({ 48 | parentNode: document.getElementById('grid'), 49 | borderDragBehavior: 'move', 50 | allowMovingSelection: true, 51 | columnHeaderClickBehavior: 'select', 52 | allowFreezingRows: true, 53 | allowFreezingColumns: true, 54 | allowRowReordering: true, 55 | tree: false, 56 | debug: false 57 | }); 58 | grid.style.height = '100%'; 59 | grid.style.width = '100%'; 60 | grid.addEventListener('beforebeginedit', function (e) { 61 | e.preventDefault(); 62 | var form = pivotForm(), 63 | schema = JSON.parse(JSON.stringify(grid.schema)); 64 | schema = schema.map(function (header) { 65 | // should check open data schema here 66 | header.type = header.enum ? 'select' : 'text'; 67 | return header; 68 | }); 69 | schema.push({ 70 | static: true, 71 | type: 'button', 72 | innerHTML: 'Ok', 73 | events: { 74 | click: function () { 75 | grid.data[e.rowIndex] = form.data; 76 | grid.draw(); 77 | form.dialog.dispose(); 78 | } 79 | } 80 | }); 81 | form.name = 'demo-form'; 82 | form.mode = 'dialog'; 83 | form.title = e.cell.data.Name; 84 | form.schema = schema; 85 | form.modal = true; 86 | form.data = e.cell.data; 87 | document.body.appendChild(form); 88 | }); 89 | grid.addEventListener('contextmenu', function (e) { 90 | e.items.push({ 91 | title: 'View page source', 92 | click: function () { window.open('https://github.com/TonyGermaneri/canvas-datagrid/blob/master/tutorials/pivotFormDemo.html', 'src'); } 93 | }); 94 | e.items.push({ 95 | title: 'View JS module', 96 | click: function () { window.open('https://github.com/TonyGermaneri/canvas-datagrid/blob/master/tutorials/pivotFormDemo.js', 'src'); } 97 | }); 98 | e.items.push({ 99 | title: 'Go to main canvas-datagrid GitHub page', 100 | click: function () { window.open('https://github.com/TonyGermaneri/canvas-datagrid', 'src'); } 101 | }); 102 | }); 103 | xhr.addEventListener('progress', function (e) { 104 | grid.data = [{ status: 'Loading data: ' + e.loaded + ' of ' + (e.total || 'unknown') + ' bytes...'}]; 105 | }); 106 | xhr.addEventListener('load', function (e) { 107 | grid.data = [{ status: 'Loading data ' + e.loaded + '...'}]; 108 | var openData = parseOpenData(JSON.parse(this.responseText)); 109 | grid.schema = openData.schema; 110 | grid.data = openData.data; 111 | }); 112 | xhr.open('GET', url); 113 | xhr.send(); 114 | } 115 | if (searchUrl.length > 3) { 116 | // work encoded or not, for lazy people who can't be bothered encoding stuff 117 | loadDataSet(/%3A/.test(searchUrl) ? decodeURIComponent(searchUrl) : searchUrl); 118 | } else { 119 | loadDataSet('https://data.cityofchicago.org/api/views/xzkq-xp2w/rows.json?accessType=DOWNLOAD'); 120 | // inner join 121 | // https://data.cityofchicago.org/api/views/pasx-mnuv/rows.json?accessType=DOWNLOAD 122 | } 123 | } 124 | if (document.addEventListener) { 125 | document.addEventListener('DOMContentLoaded', demo); 126 | } else { 127 | setTimeout(function () { 128 | 'use strict'; 129 | demo(); 130 | }, 500); 131 | } 132 | -------------------------------------------------------------------------------- /tutorials/publish.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true, stupid: true, nomen: true*/ 2 | var fs = require('fs'), 3 | path = require('path'); 4 | exports.publish = function (data) { 5 | 'use strict'; 6 | function cp(a, b) { 7 | fs.createReadStream(a).pipe(fs.createWriteStream(b)); 8 | } 9 | var files = {}, dirFiles, d = data().get(), p = __dirname, s; 10 | fs.mkdirSync(p + '/../docs/'); 11 | fs.mkdirSync(p + '/../docs/js'); 12 | fs.mkdirSync(p + '/../docs/css'); 13 | dirFiles = fs.readdirSync(p); 14 | dirFiles.forEach(function (file) { 15 | if (/\.DS_Store/.test(file)) { return; } 16 | var f = path.join(p, file); 17 | if (fs.lstatSync(f).isDirectory()) { return; } 18 | files[file] = fs.readFileSync(f, {encoding: 'utf-8'}); 19 | }); 20 | files['README.md'] = fs.readFileSync(p + '/../README.md', {encoding: 'utf-8'}); 21 | s = 'window.reflection = ' + JSON.stringify(d.filter(function (a) { 22 | return a.undocumented !== false; 23 | }), null, ' ') 24 | + ';window.files = ' + JSON.stringify(files, null, ' ') + ';'; 25 | fs.writeFileSync(p + '/../docs/js/reflection.js', s, 'utf8'); 26 | cp(path.join(p, '/../node_modules/marked/lib/marked.js'), p + '/../docs/js/marked.js'); 27 | cp(path.join(p, '/index.html'), p + '/../docs/index.html'); 28 | cp(path.join(p, '/js/main.js'), p + '/../docs/js/main.js'); 29 | cp(path.join(p, '/css/main.css'), p + '/../docs/css/main.css'); 30 | }; 31 | -------------------------------------------------------------------------------- /tutorials/reactExample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Example 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tutorials/reactExample.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, es6: true*/ 2 | class CanvasDatagrid extends React.Component { 3 | constructor(props) { 4 | super(props); 5 | } 6 | updateAttributes(nextProps) { 7 | Object.keys(this.props).forEach(key => { 8 | if (!nextProps || this.props[key] !== nextProps[key]) { 9 | if (this.grid.attributes[key] !== undefined) { 10 | this.grid.attributes[key] = nextProps ? nextProps[key] : this.props[key]; 11 | } else { 12 | this.grid[key] = nextProps ? nextProps[key] : this.props[key]; 13 | } 14 | } 15 | }); 16 | } 17 | componentWillReceiveProps(nextProps) { 18 | this.updateAttributes(nextProps); 19 | } 20 | shouldComponentUpdate() { 21 | return false; 22 | } 23 | componentWillUnmount() { 24 | this.grid.dispose(); 25 | } 26 | componentDidMount() { 27 | var args = {}; 28 | this.grid = ReactDOM.findDOMNode(this); 29 | this.updateAttributes(); 30 | } 31 | render() { 32 | return React.createElement('canvas-datagrid', {}); 33 | } 34 | } 35 | class GenerateRandomDataButton extends React.Component { 36 | constructor(props) { 37 | super(props); 38 | this.state = { data: getRandomData() }; 39 | } 40 | render() { 41 | return React.createElement('div', { 42 | style: { 43 | height: '300px' 44 | } 45 | }, 46 | React.createElement(CanvasDatagrid, { 47 | data: this.state.data 48 | }), 49 | React.createElement('button', { 50 | onClick: (e) => { this.setData(getRandomData()); } 51 | }, 'Generate Random Data') 52 | ); 53 | } 54 | setData(data) { 55 | this.setState({data}); 56 | } 57 | } 58 | function getRandomData() { 59 | return [{ 60 | foo: Math.random(), 61 | bar: Math.random(), 62 | baz: Math.random() 63 | }]; 64 | } 65 | var grid = React.createElement(GenerateRandomDataButton, {}); 66 | ReactDOM.render(grid, document.getElementById('root')); 67 | -------------------------------------------------------------------------------- /tutorials/sparklineDemo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 26 | 27 |
28 | -------------------------------------------------------------------------------- /tutorials/sparklineDemo.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true*/ 2 | /*globals canvasDatagrid: false*/ 3 | document.addEventListener('DOMContentLoaded', function () { 4 | 'use strict'; 5 | function plotSparklineChart(cell, ctx) { 6 | if (!cell.value) { return; } 7 | var g, 8 | gb, 9 | x = 0, 10 | d = (cell.value[0] - cell.value[1]).toFixed(2), 11 | m = Math.max.apply(null, cell.value), 12 | a = cell.value.reduce(function (ac, c) { return ac + c; }, 0) / cell.value.length, 13 | i = Math.min.apply(null, cell.value), 14 | w = cell.width / cell.value.length, 15 | ar = (d > 0 ? '\u25BC' : '\u25B2'), 16 | r = cell.height / (m - (m * 0.1)); 17 | function line(n, c) { 18 | ctx.beginPath(); 19 | ctx.lineWidth = 1; 20 | ctx.strokeStyle = c; 21 | ctx.moveTo(cell.x, cell.y + (n * r)); 22 | ctx.lineTo(cell.x + cell.width, cell.y + (n * r)); 23 | ctx.stroke(); 24 | } 25 | ctx.save(); 26 | gb = ctx.createLinearGradient((cell.x + cell.width) / 2, cell.y, (cell.x + cell.width) / 2, cell.y + cell.height); 27 | gb.addColorStop(0, '#0C4B73'); 28 | gb.addColorStop(1, (cell.selected || cell.active) ? '#B3C3CC' : '#041724'); 29 | ctx.fillStyle = gb; 30 | ctx.fillRect(cell.x, cell.y, cell.width, cell.height); 31 | ctx.beginPath(); 32 | ctx.moveTo(cell.x, cell.y + cell.height); 33 | cell.value.forEach(function (d) { 34 | var cx = cell.x + w + x, 35 | cy = cell.y + (d * r); 36 | ctx.lineTo(cx, cy); 37 | if (d === i || d === m) { 38 | ctx.fillStyle = d === m ? 'green' : 'red'; 39 | ctx.fillRect(cx - 2, cy - 2, 5, 5); 40 | } 41 | x += w; 42 | }); 43 | ctx.lineTo(cell.x + cell.width, cell.y + cell.height); 44 | g = ctx.createLinearGradient((cell.x + cell.width) / 2, cell.y, (cell.x + cell.width) / 2, cell.y + cell.height); 45 | g.addColorStop(0, '#0F5C8C'); 46 | g.addColorStop(1, '#499ABA'); 47 | ctx.fillStyle = g; 48 | ctx.fill(); 49 | ctx.strokeStyle = '#0B466B'; 50 | ctx.stroke(); 51 | line(a, d >= 0 ? 'green' : 'red'); 52 | cell.parentGrid.data[cell.rowIndex].col1 = (d === 0 ? ' ' : ar) + ' Diff: ' + d 53 | + 'Avg:' + a.toFixed(2) + '\nMin: ' + i.toFixed(2) + '\nMax: ' + m.toFixed(2); 54 | ctx.restore(); 55 | } 56 | function createRandomSeq(size, r) { 57 | r = r || []; 58 | while (r.length < size) { 59 | r.push(Math.random()); 60 | } 61 | return r; 62 | } 63 | // create a new grid 64 | var grid = canvasDatagrid({ 65 | parentNode: document.getElementById('grid'), 66 | schema: [ 67 | {name: 'col1', width: 330}, 68 | {name: 'col2', width: 300}, 69 | {name: 'col3', width: 300} 70 | ] 71 | }); 72 | grid.sizes.rows[2] = 200; 73 | grid.sizes.columns[1] = 600; 74 | grid.sizes.columns[2] = 400; 75 | grid.addEventListener('formattext', function (e) { 76 | if (e.cell.columnIndex < 1) { return; } 77 | e.preventDefault(); 78 | e.cell.text = { lines: [{value: e.cell.value }] }; 79 | }); 80 | grid.style.height = '100%'; 81 | grid.style.width = '100%'; 82 | grid.addEventListener('contextmenu', function (e) { 83 | e.items.push({ 84 | title: 'View page source', 85 | click: function () { window.open('https://github.com/TonyGermaneri/canvas-datagrid/blob/master/tutorials/sparklineDemo.html', 'src'); } 86 | }); 87 | e.items.push({ 88 | title: 'View JS module', 89 | click: function () { window.open('https://github.com/TonyGermaneri/canvas-datagrid/blob/master/tutorials/sparklineDemo.js', 'src'); } 90 | }); 91 | e.items.push({ 92 | title: 'Go to main canvas-datagrid GitHub page', 93 | click: function () { window.open('https://github.com/TonyGermaneri/canvas-datagrid', 'src'); } 94 | }); 95 | }); 96 | grid.addEventListener('beforebeginedit', function (e) { 97 | e.preventDefault(); 98 | }); 99 | grid.addEventListener('beforerendercell', function (e) { 100 | if (/col2|col3/.test(e.header.name) && !e.cell.isHeader) { 101 | e.cell.isGrid = false; 102 | } 103 | }); 104 | grid.addEventListener('rendertext', function (e) { 105 | if (/col2|col3/.test(e.header.name) && !e.cell.isHeader) { 106 | e.preventDefault(); 107 | } 108 | if (!e.cell.isHeader && e.cell.value && e.cell.value.substring) { 109 | e.ctx.fillStyle = /Diff: -/.test(e.cell.value) ? '#499A3D' : '#A1230F'; 110 | } 111 | }); 112 | grid.addEventListener('afterrendercell', function (e) { 113 | if (/col2|col3/.test(e.header.name) && !e.cell.isHeader) { 114 | plotSparklineChart(e.cell, e.ctx); 115 | e.preventDefault(); 116 | } 117 | }); 118 | grid.data = (function () { 119 | var d = [], x = 0; 120 | while (x < 2000) { 121 | d.push({col1: 'Avg: 0.50\nMin: 0\nMax: 0.99', col2: createRandomSeq(80), col3: createRandomSeq(100)}); 122 | x += 1; 123 | } 124 | return d; 125 | }()); 126 | function getData(fill) { 127 | var a, x = grid.scrollIndexRect.top; 128 | while (x < grid.scrollIndexRect.bottom + 2) { 129 | if (fill || grid.isCellVisible(1, x)) { 130 | a = grid.data[x].col2; 131 | a.shift(); 132 | a.push(Math.random()); 133 | a = grid.data[x].col3; 134 | a.shift(); 135 | a.push(Math.random()); 136 | } 137 | x += 1; 138 | } 139 | grid.draw(); 140 | pollData(); 141 | } 142 | function pollData() { 143 | setTimeout(getData, 50); 144 | } 145 | getData(true); 146 | }); -------------------------------------------------------------------------------- /tutorials/styleBuilder.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /tutorials/vueExample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue Example 6 | 7 | 8 |
9 | 10 | 11 |
Value of row 1 col 1: {{grid.data[0].col1}}
12 |
13 | 14 |

Vue usage

15 |
    16 |
  • Install the package: 17 |
    npm install canvas-datagrid
    18 |
  • 19 |
  • In main.js: 20 |
    import 'canvas-datagrid';
    21 |                 Vue.config.ignoredElements = ['canvas-datagrid']; // Suppresses warnings about unknown tags in Vue.
    22 |
  • 23 |
  • In your component:
  • 24 |
      25 |
    • Add <canvas-datagrid/> to your template and pass props as follows: <canvas-datagrid :data.prop="rows"/>.
    • 26 |
    27 |
  • Notes: 28 |
      29 |
    • Canvas-datagrid is event based like any DOM element so it works with Vue's reactivity natively.
    • 30 |
    • Make sure you use the .prop suffix on properties that require non string values. e.g.: v-bind.prop="myData"
    • 31 |
    • If you're getting errors about JSON parsing you might be inadvertently passing values in between the tag: <canvas-datagrid>...</canvas-datagrid> tag.
      The grid will interpret this as JSON, invalid JSON will throw an error.
    • 32 |
    • If you pass data in as innerHTML (between the open and close tag) it will lose reactivity as it is string data.
    • 33 |
    34 |
  • 35 |
36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tutorials/vueExample.js: -------------------------------------------------------------------------------- 1 | Vue.config.ignoredElements = ['canvas-datagrid']; 2 | new Vue({ 3 | el: '#app', 4 | data: { 5 | grid: { 6 | data: [ 7 | {col1: 'foo', col2: 0, col3: 'a'}, 8 | {col1: 'bar', col2: 1, col3: 'b'}, 9 | {col1: 'baz', col2: 2, col3: 'c'} 10 | ] 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /tutorials/webcomponentDemo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 22 | 23 | 24 | [ 25 | {"col1": "row 1 column 1", "col2": "row 1 column 2", "col3": "row 1 column 3"}, 26 | {"col1": "row 2 column 1", "col2": "row 2 column 2", "col3": "row 2 column 3"} 27 | ] 28 | 29 | 30 | -------------------------------------------------------------------------------- /tutorials/xhrPagingDemo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 26 | 27 |
28 | -------------------------------------------------------------------------------- /tutorials/xhrPagingDemo.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true*/ 2 | /*globals canvasDatagrid: false*/ 3 | function demo() { 4 | 'use strict'; 5 | var grid, dataCache, scrollDebounce, loadingText = 'Loading...'; 6 | function intializeCache() { 7 | var x; 8 | dataCache = []; 9 | // the API does not return the number of records, but this is apparently the number 10 | // it's important to set the length of the data correctly 11 | // to allow the scroll box to be the correct height 12 | // if not, you can't reach the last record 13 | // Most APIs provide the total number of records in the set 14 | // this API does not, so it is hard coded here 15 | dataCache.length = 156800; 16 | for (x = 0; x < dataCache.length; x += 1) { 17 | // create empty "loading..." rows 18 | dataCache[x] = { 19 | id: x, 20 | loaded: false, 21 | category: loadingText, 22 | question: loadingText, 23 | answer: loadingText, 24 | value: 0, 25 | airdate: 0 26 | }; 27 | } 28 | // set the stub data to the grid. We'll mutate the data later as pages load 29 | grid.data = dataCache; 30 | // adjust the schema based on the key indexes 31 | // defined above (id, loaded, category, etc..) so it looks nice 32 | grid.schema[0].hidden = true; 33 | grid.schema[1].hidden = true; 34 | grid.schema[2].width = 150; 35 | grid.schema[3].width = 400; 36 | grid.schema[4].width = 250; 37 | grid.schema[5].width = 75; 38 | grid.schema[6].width = 100; 39 | // set the type of the number and date columns for good form 40 | // if this was a sortable table this would actually matter 41 | grid.schema[5].type = 'number'; 42 | // we will use the date type to bind this column to a formatting function later 43 | grid.schema[6].type = 'date'; 44 | } 45 | function updateLocalCache(data, offset) { 46 | var d, x; 47 | // merge data from remote into local cache data 48 | for (x = 0; x < data.length; x += 1) { 49 | d = dataCache[x + offset]; 50 | d.loaded = true; 51 | d.category = data[x].category.title; 52 | d.question = data[x].question; 53 | d.answer = data[x].answer; 54 | d.value = data[x].value || 0; 55 | d.airdate = data[x].airdate; 56 | } 57 | // the grid cannot detect when the data has changed 58 | // so draw is called after data update 59 | grid.draw(); 60 | } 61 | function dataAdapter(offset, rows, callback) { 62 | // THANKS TO http://jservice.io for providing this free service!! 63 | var url, xhr = new XMLHttpRequest(); 64 | // bind an event to change the cursor to let the user know it's loading 65 | xhr.addEventListener('progress', function () { 66 | document.body.style.cursor = 'wait'; 67 | }); 68 | // onload, parse the object, change the cursor back and call the callback (updateLocalCache) 69 | xhr.addEventListener('load', function () { 70 | var data = JSON.parse(this.responseText); 71 | document.body.style.cursor = 'auto'; 72 | callback(data, offset, rows); 73 | }); 74 | // rows is not used by this API, other APIs usually define some sort of records per page 75 | url = 'http://jservice.io/api/clues?offset=:offset' 76 | .replace(':offset', offset) 77 | .replace(':rows', rows); 78 | xhr.open('GET', url); 79 | xhr.send(); 80 | } 81 | // instantiate the grid 82 | grid = canvasDatagrid(); 83 | grid.attributes.globalRowResize = true; 84 | grid.style.cellWhiteSpace = 'normal'; 85 | grid.style.cellHeight = 80; 86 | grid.style.height = '100%'; 87 | grid.style.width = '100%'; 88 | // format dates so they are easy to read 89 | // grid.formatters. determine how data 90 | // of a certain type is formatted as it is drawn 91 | grid.formatters.date = function (e) { 92 | if (!e.value) { return ''; } 93 | var d = new Date(e.value); 94 | return d.getMonth() + '/' + d.getDay() + '/' + d.getFullYear(); 95 | }; 96 | // API does not support filtering, so remove the option from the context menu; 97 | grid.attributes.showFilter = false; 98 | // stick the grid in the
element defined in the HTML doc 99 | document.getElementById('grid').appendChild(grid); 100 | // warn people reordering does not work in this free sample API 101 | // (but if it did, you could implement it here) 102 | grid.addEventListener('click', function (e) { 103 | if (e.cell.rowIndex === -1 && e.cell.columnIndex > -1) { 104 | alert('The free jservice.io API does not support ordering or filtering'); 105 | e.preventDefault(); 106 | } 107 | }); 108 | // attach a scroll event to the grid, will fire when the scroll box changes 109 | grid.addEventListener('scroll', function () { 110 | // ensure we don't overwhelm the API with requests from quickly scrolling 111 | clearTimeout(scrollDebounce); 112 | scrollDebounce = setTimeout(function () { 113 | // check what part of the virtual canvas is visible by using grid.scrollIndexRect 114 | // then check the records in that set to see if they need to be fetched or if they 115 | // are already in the cache 116 | if (grid.data.filter(function (d, i) { 117 | return i > grid.scrollIndexRect.top && i < grid.scrollIndexRect.bottom && d.loaded === false; 118 | }).length > 0) { 119 | var offset = grid.scrollIndexRect.top, 120 | rows = grid.scrollIndexRect.bottom - grid.scrollIndexRect.top; 121 | dataAdapter(offset, rows, updateLocalCache); 122 | } 123 | }, 300); 124 | }); 125 | // create rows of empty data the size of the data to later stick data into 126 | intializeCache(); 127 | // grab the first page 128 | dataAdapter(0, 100, updateLocalCache); 129 | } 130 | document.addEventListener('DOMContentLoaded', demo); 131 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const productionConfig = { 4 | mode: 'production', 5 | 6 | entry: './lib/main.js', 7 | 8 | module: { 9 | rules: [{ test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' }], 10 | }, 11 | 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | library: 'canvasDatagrid', 15 | libraryTarget: 'umd', 16 | libraryExport: 'default', 17 | filename: 'canvas-datagrid.js', 18 | }, 19 | }; 20 | 21 | const developmentConfig = { 22 | ...productionConfig, 23 | 24 | mode: 'development', 25 | devtool: 'source-map', 26 | devServer: { 27 | contentBase: [ 28 | path.join(__dirname, 'dist'), 29 | path.join(__dirname, 'tutorials'), 30 | ], 31 | }, 32 | 33 | output: { 34 | path: path.resolve(__dirname, 'dist'), 35 | library: 'canvasDatagrid', 36 | libraryTarget: 'umd', 37 | libraryExport: 'default', 38 | filename: 'canvas-datagrid.debug.js', 39 | sourceMapFilename: 'canvas-datagrid.debug.map', 40 | }, 41 | }; 42 | 43 | module.exports = [productionConfig, developmentConfig]; 44 | --------------------------------------------------------------------------------