├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── beta.js.yml │ ├── node.js.yml │ ├── release.js.yml │ └── storybook.js.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── .storybook ├── .babelrc.json ├── addons │ └── expand-all │ │ └── register.mjs ├── main.js ├── main.ts ├── manager.ts ├── preview-body.html ├── preview-head.html └── preview.ts ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.json ├── config ├── build-util.sh └── linaria.json ├── data-grid.jpg ├── icon.png ├── media ├── data-grid-dark.png ├── data-grid.png └── icon.png ├── package-lock.json ├── package.json ├── packages ├── cells │ ├── .eslintignore │ ├── .eslintrc │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── build.sh │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── cell.stories.tsx │ │ ├── cells │ │ │ ├── article-cell-editor.tsx │ │ │ ├── article-cell-types.ts │ │ │ ├── article-cell.tsx │ │ │ ├── button-cell.tsx │ │ │ ├── date-picker-cell.tsx │ │ │ ├── dropdown-cell.tsx │ │ │ ├── links-cell.tsx │ │ │ ├── multi-select-cell.tsx │ │ │ ├── range-cell.tsx │ │ │ ├── sparkline-cell.tsx │ │ │ ├── spinner-cell.tsx │ │ │ ├── star-cell.tsx │ │ │ ├── tags-cell.tsx │ │ │ ├── tree-view-cell.tsx │ │ │ └── user-profile-cell.tsx │ │ ├── draw-fns.ts │ │ └── index.ts │ ├── test │ │ ├── date-picker-cell.test.tsx │ │ └── multi-select-cell.test.tsx │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts ├── core │ ├── .eslintignore │ ├── .eslintrc │ ├── .npmignore │ ├── API.md │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── build.sh │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── cells │ │ │ ├── boolean-cell.tsx │ │ │ ├── bubble-cell.tsx │ │ │ ├── cell-types.ts │ │ │ ├── drilldown-cell.tsx │ │ │ ├── image-cell.tsx │ │ │ ├── index.ts │ │ │ ├── loading-cell.tsx │ │ │ ├── markdown-cell.tsx │ │ │ ├── marker-cell.tsx │ │ │ ├── new-row-cell.tsx │ │ │ ├── number-cell.tsx │ │ │ ├── protected-cell.tsx │ │ │ ├── row-id-cell.tsx │ │ │ ├── text-cell.tsx │ │ │ └── uri-cell.tsx │ │ ├── common │ │ │ ├── browser-detect.ts │ │ │ ├── image-window-loader.ts │ │ │ ├── is-hotkey.ts │ │ │ ├── math.ts │ │ │ ├── render-state-provider.ts │ │ │ ├── resize-detector.ts │ │ │ ├── styles.ts │ │ │ ├── support.ts │ │ │ └── utils.tsx │ │ ├── data-editor-all.tsx │ │ ├── data-editor │ │ │ ├── copy-paste.ts │ │ │ ├── data-editor-fns.ts │ │ │ ├── data-editor-keybindings.ts │ │ │ ├── data-editor.tsx │ │ │ ├── group-rename.tsx │ │ │ ├── row-grouping-api.ts │ │ │ ├── row-grouping.ts │ │ │ ├── stories │ │ │ │ ├── data-editor-repros.stories.tsx │ │ │ │ ├── data-editor.stories.tsx │ │ │ │ └── utils.tsx │ │ │ ├── use-autoscroll.ts │ │ │ ├── use-cells-for-selection.ts │ │ │ ├── use-column-sizer.ts │ │ │ ├── use-initial-scroll-offset.ts │ │ │ ├── use-rem-adjuster.ts │ │ │ └── visible-region.ts │ │ ├── docs │ │ │ ├── 00-faq.stories.tsx │ │ │ ├── 01-getting-started.stories.tsx │ │ │ ├── 02-editing-data.stories.tsx │ │ │ ├── 03-grid-column.stories.tsx │ │ │ ├── 04-streaming-data.stories.tsx │ │ │ ├── 05-copy-paste.stories.tsx.tsx │ │ │ ├── 06-search.stories.tsx │ │ │ ├── 07-column-grouping.stories.tsx │ │ │ ├── 08-theming.stories.tsx │ │ │ ├── 09-menus.stories.tsx │ │ │ ├── doc-wrapper.tsx │ │ │ ├── examples │ │ │ │ ├── add-column.stories.tsx │ │ │ │ ├── add-data-to-middle.stories.tsx │ │ │ │ ├── add-data-to-top.stories.tsx │ │ │ │ ├── add-data.stories.tsx │ │ │ │ ├── all-cell-kinds.stories.tsx │ │ │ │ ├── append-row-handle.stories.tsx │ │ │ │ ├── automatic-row-markers.stories.tsx │ │ │ │ ├── built-in-search.stories.tsx │ │ │ │ ├── cell-activated-event.stories.tsx │ │ │ │ ├── column-group-collapse.stories.tsx │ │ │ │ ├── column-groups.stories.tsx │ │ │ │ ├── content-alignment.stories.tsx │ │ │ │ ├── controlled-search.stories.tsx │ │ │ │ ├── controlled-selection.stories.tsx │ │ │ │ ├── copy-support.stories.tsx │ │ │ │ ├── custom-event-target.stories.tsx │ │ │ │ ├── custom-header-icons.stories.tsx │ │ │ │ ├── custom-header.stories.tsx │ │ │ │ ├── drag-source.stories.tsx │ │ │ │ ├── drop-events.stories.tsx │ │ │ │ ├── fill-handle.stories.tsx │ │ │ │ ├── freeze-columns.stories.tsx │ │ │ │ ├── freeze-rows.stories.tsx │ │ │ │ ├── header-menus.stories.tsx │ │ │ │ ├── highlight-cells.stories.tsx │ │ │ │ ├── imperative-scroll.stories.tsx │ │ │ │ ├── input-blending.stories.tsx │ │ │ │ ├── keybindings.stories.tsx │ │ │ │ ├── layout-integration.stories.tsx │ │ │ │ ├── multi-select-columns.stories.tsx │ │ │ │ ├── new-column-button.stories.tsx │ │ │ │ ├── obscured-grid.stories.tsx │ │ │ │ ├── observe-visible-region.stories.tsx │ │ │ │ ├── one-hundred-thousand-columns.stories.tsx │ │ │ │ ├── one-million-rows.stories.tsx │ │ │ │ ├── overscroll.stories.tsx │ │ │ │ ├── padding.stories.tsx │ │ │ │ ├── paste-support.stories.tsx │ │ │ │ ├── prevent-diagonal-scroll.stories.tsx │ │ │ │ ├── rapid-updates.stories.tsx │ │ │ │ ├── rearrange-columns.stories.tsx │ │ │ │ ├── reorder-rows.stories.tsx │ │ │ │ ├── resizable-columns.stories.tsx │ │ │ │ ├── right-element.stories.tsx │ │ │ │ ├── right-to-left.stories.tsx │ │ │ │ ├── row-and-header-sizes.stories.tsx │ │ │ │ ├── row-grouping.stories.tsx │ │ │ │ ├── row-hover.stories.tsx │ │ │ │ ├── row-markers.stories.tsx │ │ │ │ ├── row-selections.stories.tsx │ │ │ │ ├── scaled-view.stories.tsx │ │ │ │ ├── scroll-offset.stories.tsx │ │ │ │ ├── scroll-shadows.stories.tsx │ │ │ │ ├── search-as-filter.stories.tsx │ │ │ │ ├── server-side-data.stories.tsx │ │ │ │ ├── shadow-dom.stories.tsx │ │ │ │ ├── silly-numbers.stories.tsx │ │ │ │ ├── small-editable-grid.stories.tsx │ │ │ │ ├── smooth-scrolling-grid.stories.tsx │ │ │ │ ├── span-cell.stories.tsx │ │ │ │ ├── stretch-column-size.stories.tsx │ │ │ │ ├── ten-million-cells.stories.tsx │ │ │ │ ├── theme-per-column.stories.tsx │ │ │ │ ├── theme-per-row.stories.tsx │ │ │ │ ├── theme-support.stories.tsx │ │ │ │ ├── tooltips.stories.tsx │ │ │ │ ├── trailing-row-options.stories.tsx │ │ │ │ ├── uneven-rows.stories.tsx │ │ │ │ ├── validate-data.stories.tsx │ │ │ │ └── wrapping-text.stories.tsx │ │ │ └── template.tsx │ │ ├── index.ts │ │ ├── internal │ │ │ ├── click-outside-container │ │ │ │ └── click-outside-container.tsx │ │ │ ├── data-editor-container │ │ │ │ └── data-grid-container.tsx │ │ │ ├── data-grid-dnd │ │ │ │ └── data-grid-dnd.tsx │ │ │ ├── data-grid-overlay-editor │ │ │ │ ├── data-grid-overlay-editor-style.tsx │ │ │ │ ├── data-grid-overlay-editor.tsx │ │ │ │ ├── private │ │ │ │ │ ├── bubbles-overlay-editor-style.tsx │ │ │ │ │ ├── bubbles-overlay-editor.tsx │ │ │ │ │ ├── drilldown-overlay-editor.tsx │ │ │ │ │ ├── image-overlay-editor-style.tsx │ │ │ │ │ ├── image-overlay-editor.tsx │ │ │ │ │ ├── markdown-overlay-editor-style.tsx │ │ │ │ │ ├── markdown-overlay-editor.tsx │ │ │ │ │ ├── number-overlay-editor-style.tsx │ │ │ │ │ ├── number-overlay-editor.tsx │ │ │ │ │ ├── uri-overlay-editor-style.tsx │ │ │ │ │ └── uri-overlay-editor.tsx │ │ │ │ └── use-stay-on-screen.ts │ │ │ ├── data-grid-search │ │ │ │ ├── data-grid-search-style.tsx │ │ │ │ └── data-grid-search.tsx │ │ │ ├── data-grid │ │ │ │ ├── animation-manager.ts │ │ │ │ ├── cell-set.ts │ │ │ │ ├── color-parser.ts │ │ │ │ ├── data-grid-sprites.ts │ │ │ │ ├── data-grid-types.ts │ │ │ │ ├── data-grid.stories.tsx │ │ │ │ ├── data-grid.tsx │ │ │ │ ├── event-args.ts │ │ │ │ ├── image-window-loader-interface.ts │ │ │ │ ├── render │ │ │ │ │ ├── data-grid-lib.ts │ │ │ │ │ ├── data-grid-render.blit.ts │ │ │ │ │ ├── data-grid-render.cells.ts │ │ │ │ │ ├── data-grid-render.header.ts │ │ │ │ │ ├── data-grid-render.lines.ts │ │ │ │ │ ├── data-grid-render.ts │ │ │ │ │ ├── data-grid-render.walk.ts │ │ │ │ │ ├── data-grid.render.rings.ts │ │ │ │ │ ├── draw-checkbox.ts │ │ │ │ │ ├── draw-edit-hover-indicator.ts │ │ │ │ │ └── draw-grid-arg.ts │ │ │ │ ├── sprites.ts │ │ │ │ ├── use-animation-queue.ts │ │ │ │ └── use-selection-behavior.ts │ │ │ ├── growing-entry │ │ │ │ ├── growing-entry-style.tsx │ │ │ │ └── growing-entry.tsx │ │ │ ├── markdown-div │ │ │ │ ├── markdown-div.tsx │ │ │ │ └── private │ │ │ │ │ └── markdown-container.tsx │ │ │ └── scrolling-data-grid │ │ │ │ ├── infinite-scroller.tsx │ │ │ │ ├── scrolling-data-grid.stories.tsx │ │ │ │ ├── scrolling-data-grid.tsx │ │ │ │ └── use-kinetic-scroll.ts │ │ └── stories │ │ │ └── story-utils.tsx │ ├── test │ │ ├── animation-manager.test.ts │ │ ├── cells.test.tsx │ │ ├── click-outside-container.test.tsx │ │ ├── color-parser.test.ts │ │ ├── common.test.ts │ │ ├── copy-paste.test.ts │ │ ├── data-editor-fns.test.ts │ │ ├── data-editor-input.test.tsx │ │ ├── data-editor-resize.test.tsx │ │ ├── data-editor.test.tsx │ │ ├── data-grid-lib.test.ts │ │ ├── data-grid-overlay.test.tsx │ │ ├── data-grid-types.test.ts │ │ ├── data-grid.test.tsx │ │ ├── image-window-loader.test.ts │ │ ├── math.test.ts │ │ ├── render-state-provider.test.ts │ │ ├── row-grouping.test.ts │ │ ├── test-utils.tsx │ │ ├── uri-cell.test.ts │ │ ├── use-animation-queue.test.ts │ │ ├── use-autoscroll.test.tsx │ │ ├── use-column-sizer.test.tsx │ │ ├── use-deep-memo.test.ts │ │ ├── use-kinetic-scroll.test.ts │ │ ├── use-rem-adjuster.test.ts │ │ └── utils.test.ts │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts └── source │ ├── .eslintignore │ ├── .eslintrc │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── build.sh │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── index.ts │ ├── stories │ │ ├── use-data-source.stories.tsx │ │ └── utils.tsx │ ├── use-async-data-source.ts │ ├── use-collapsing-groups.ts │ ├── use-column-sort.ts │ ├── use-movable-columns.ts │ └── use-undo-redo.ts │ ├── test │ ├── use-column-sort.test.tsx │ └── use-data-source.test.tsx │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts ├── setup-react-18-test.sh ├── test-projects ├── bootstrap-projects.sh ├── cra5-gdg │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── components │ │ │ └── Grid.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ └── setupTests.ts │ └── tsconfig.json └── next-gdg │ ├── .babelrc │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── components │ └── Grid.tsx │ ├── next-env.d.ts │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── api │ │ └── hello.ts │ └── index.tsx │ ├── public │ ├── favicon.ico │ └── vercel.svg │ ├── styles │ ├── Home.module.css │ └── globals.css │ └── tsconfig.json ├── tsconfig.json └── update-version.sh /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.158.0/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version: 14, 12, 10 4 | ARG VARIANT="14-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg 8 | RUN echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null 9 | 10 | RUN apt-get update && DEBIAN_FRONTEND=non-interactive apt-get install gh jq -y -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.158.0/containers/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 10, 12, 14 8 | "args": { 9 | "VARIANT": "14" 10 | } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": { 15 | "terminal.integrated.shell.linux": "/bin/bash" 16 | }, 17 | 18 | // Add the IDs of extensions you want installed when the container is created. 19 | "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"], 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | "forwardPorts": [9009], 23 | 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | "postCreateCommand": "./.devcontainer/run.sh", 26 | 27 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 28 | "remoteUser": "node" 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/node.js.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: Build 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Use Node.js 20 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 20.10.0 17 | - run: npm install 18 | - run: npm run build 19 | - run: npm run test -- --coverage 20 | - run: npm run test-source 21 | - run: npm run test-cells 22 | - run: npm run test-projects 23 | - name: Coveralls 24 | uses: coverallsapp/github-action@master 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | path-to-lcov: ./packages/core/coverage/lcov.info 28 | base-path: ./packages/core 29 | test-react-18: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-node@v4 34 | with: 35 | node-version: 20.10.0 36 | - run: npm install 37 | - run: npm run test-18 38 | -------------------------------------------------------------------------------- /.github/workflows/release.js.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: Release CI 5 | 6 | on: 7 | push: 8 | tags: 9 | - "v*" 10 | - "!v*-*" 11 | 12 | jobs: 13 | publish-core: 14 | defaults: 15 | run: 16 | working-directory: ./packages/core 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20.10.0 23 | - name: Bootstrap 24 | run: npm install && npm run build 25 | working-directory: . 26 | - uses: JS-DevTools/npm-publish@v1 27 | with: 28 | token: ${{ secrets.NPM_TOKEN }} 29 | access: public 30 | package: ./packages/core/package.json 31 | publish-cells: 32 | defaults: 33 | run: 34 | working-directory: ./packages/cells 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: actions/setup-node@v4 39 | with: 40 | node-version: 20.10.0 41 | - name: Bootstrap 42 | run: npm install && npm run build 43 | working-directory: . 44 | - uses: JS-DevTools/npm-publish@v1 45 | with: 46 | token: ${{ secrets.NPM_TOKEN }} 47 | access: public 48 | package: ./packages/cells/package.json 49 | publish-source: 50 | defaults: 51 | run: 52 | working-directory: ./packages/source 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v4 56 | - uses: actions/setup-node@v4 57 | with: 58 | node-version: 20.10.0 59 | - name: Bootstrap 60 | run: npm install && npm run build 61 | working-directory: . 62 | - uses: JS-DevTools/npm-publish@v1 63 | with: 64 | token: ${{ secrets.NPM_TOKEN }} 65 | access: public 66 | package: ./packages/source/package.json 67 | -------------------------------------------------------------------------------- /.github/workflows/storybook.js.yml: -------------------------------------------------------------------------------- 1 | name: Storybook Build and Deploy 2 | on: 3 | push: 4 | branches: ["main"] 5 | paths: ["packages/core/**", "packages/cells/**", "packages/source/**"] 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v4 12 | with: 13 | persist-credentials: false 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 20.10.0 17 | - name: Install and Build 🔧 18 | run: | 19 | npm install 20 | npm run build-storybook 21 | - name: "Add .nojekyll 📄" 22 | run: cd storybook-build && touch .nojekyll 23 | - name: Deploy 🚀 24 | uses: JamesIves/github-pages-deploy-action@3.6.2 25 | with: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | BRANCH: gh-pages 28 | FOLDER: storybook-build 29 | CLEAN: true 30 | TARGET_FOLDER: docs 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Dd]ist/ 2 | node_modules* 3 | .DS_Store 4 | .tool-versions 5 | .idea 6 | .cache 7 | pubsub-debug.log 8 | storybook-build/ 9 | coverage/ 10 | *.tsbuildinfo 11 | gen-docs/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.10.0 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "printWidth": 120, 4 | "semi": true, 5 | "jsxBracketSameLine": true, 6 | "arrowParens": "avoid", 7 | "bracketSpacing": true, 8 | "cursorOffset": -1, 9 | "endOfLine": "lf", 10 | "htmlWhitespaceSensitivity": "css", 11 | "insertPragma": false, 12 | "jsxSingleQuote": false, 13 | "proseWrap": "preserve", 14 | "quoteProps": "as-needed", 15 | "rangeStart": 0, 16 | "requirePragma": false, 17 | "singleQuote": false, 18 | "trailingComma": "es5", 19 | "useTabs": false, 20 | "vueIndentScriptAndStyle": false 21 | } 22 | -------------------------------------------------------------------------------- /.storybook/.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "assumptions": { 3 | "setPublicClassFields": true, 4 | "setSpreadProperties": true 5 | }, 6 | "sourceType": "unambiguous", 7 | "presets": [ 8 | [ 9 | "@babel/preset-env", 10 | { 11 | "targets": { 12 | "firefox": "60", 13 | "chrome": "67", 14 | "safari": "12.1" 15 | }, 16 | "exclude": ["@babel/plugin-transform-template-literals"] 17 | } 18 | ], 19 | "@babel/preset-react", 20 | "@babel/preset-typescript", 21 | "@linaria" 22 | ], 23 | "plugins": ["@babel/plugin-proposal-class-properties"] 24 | } 25 | -------------------------------------------------------------------------------- /.storybook/addons/expand-all/register.mjs: -------------------------------------------------------------------------------- 1 | import { STORY_RENDERED } from "@storybook/core-events"; 2 | import { addons } from "@storybook/addons"; 3 | 4 | let hasExpanded = false; 5 | 6 | addons.register("expand-all", api => { 7 | const emitter = addons.getChannel(); 8 | 9 | emitter.on(STORY_RENDERED, () => { 10 | if (!hasExpanded) { 11 | setTimeout(api.expandAll); // Calling on the next tick after storyRendered seems to work reliably. 12 | hasExpanded = true; 13 | } 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | stories: ["../**/src/**/*.stories.tsx"], 5 | addons: [getAbsolutePath("@storybook/addon-storysource"), getAbsolutePath("@storybook/addon-controls")], 6 | 7 | typescript: { 8 | reactDocgen: false, 9 | }, 10 | 11 | async viteFinal(config) { 12 | // We need to dynamically import these since they use ESM 13 | const { mergeConfig } = await import("vite"); 14 | const linaria = (await import("@linaria/vite")).default; 15 | 16 | return mergeConfig(config, { 17 | plugins: [linaria()], 18 | }); 19 | }, 20 | 21 | framework: { 22 | name: getAbsolutePath("@storybook/react-vite"), 23 | options: {}, 24 | }, 25 | 26 | docs: { 27 | autodocs: false, 28 | }, 29 | }; 30 | 31 | function getAbsolutePath(value) { 32 | return path.dirname(require.resolve(path.join(value, "package.json"))); 33 | } 34 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from "path"; 2 | import linaria from "@linaria/vite"; 3 | import { mergeConfig } from "vite"; 4 | 5 | export default { 6 | stories: ["../**/src/**/*.stories.tsx"], 7 | addons: [getAbsolutePath("@storybook/addon-storysource"), getAbsolutePath("@storybook/addon-controls")], 8 | 9 | typescript: { 10 | reactDocgen: false, 11 | }, 12 | 13 | async viteFinal(config) { 14 | return mergeConfig(config, { 15 | plugins: [linaria()], 16 | }); 17 | }, 18 | 19 | framework: { 20 | name: getAbsolutePath("@storybook/react-vite"), 21 | options: {}, 22 | }, 23 | 24 | docs: { 25 | autodocs: false, 26 | }, 27 | }; 28 | 29 | function getAbsolutePath(value) { 30 | return dirname(require.resolve(join(value, "package.json"))); 31 | } 32 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/addons"; 2 | import { create } from "@storybook/theming"; 3 | 4 | const glideTheme = create({ 5 | base: "dark", 6 | brandTitle: "Glide Data Grid", 7 | brandUrl: "https://grid.glideapps.com", 8 | brandImage: "https://res.cloudinary.com/glide/image/upload/c_scale,w_45/v1634058004/glidehq/glide-transparent.png", 9 | }); 10 | 11 | addons.setConfig({ 12 | isFullscreen: false, 13 | showNav: true, 14 | showPanel: false, 15 | panelPosition: "right", 16 | enableShortcuts: true, 17 | isToolshown: false, 18 | theme: glideTheme, 19 | selectedPanel: undefined, 20 | initialActive: "sidebar", 21 | sidebar: { 22 | showRoots: true, 23 | collapsedRoots: ["Subcomponents", "TestCases"], 24 | }, 25 | toolbar: { 26 | title: { hidden: false }, 27 | zoom: { hidden: false }, 28 | eject: { hidden: false }, 29 | copy: { hidden: false }, 30 | fullscreen: { hidden: false }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /.storybook/preview-body.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | const preview = { 2 | parameters: { 3 | layout: "fullscreen", 4 | options: { 5 | storySort: { 6 | method: "alphabetical", 7 | order: ["Glide-Data-Grid", "Extra Packages", "Subcomponents"], 8 | locales: "en-US", 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default preview; 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "msedge", 6 | "request": "launch", 7 | "name": "Launch Glide Data Grid", 8 | "url": "http://localhost:9009", 9 | "webRoot": "${workspaceRoot}/", 10 | "preLaunchTask": "npm: start", 11 | "sourceMaps": true 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": ["packages/core", "packages/cells", "packages/source"], 3 | "typescript.tsdk": "node_modules/typescript/lib" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "npm: start", 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | 10 | "problemMatcher": { 11 | "pattern": { 12 | "regexp": ".*" 13 | }, 14 | "background": { 15 | "activeOnStart": true, 16 | "beginsPattern": "^.*Starting Storybook.*$", // Adjust if there's a more specific start message 17 | "endsPattern": "^.*Storybook \\d+.\\d+.\\d+ for react-webpack5 started.*$" 18 | } 19 | }, 20 | "group": { 21 | "kind": "build", 22 | "isDefault": true 23 | }, 24 | "presentation": { 25 | "reveal": "always", 26 | "panel": "new" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Getting set up to work on Glide Data Grid 2 | 3 | ### Setting Up Codespaces 4 | 5 | If you'd like to set up glide data grid locally and contribute, the easiest way to get up and running 6 | is to use Codespaces if you have access to it. If you do not, simply cloning the repo and running `npm install` also works! 7 | 8 | #### Steps 9 | 10 | - Click the green dropdown labeled code, there should be two tabs: local and codespaces. 11 | - Click on codespaces. 12 | - If this is your first time, then create a new codespace. It will open a new browser tab and build the docker container for it - there will be a button to open the environment in VSCode if you'd prefer to run it that way 13 | - You should see a screen that says `Setting up your codespace` As soon as that's done, you should see a VSCode like UI with files on the left. 14 | 15 | Once codespaces is up and running make sure `jq` is installed and then: 16 | 17 | ```bash 18 | npm run install && npm run storybook 19 | ``` 20 | 21 | ## Forking the data grid? 22 | 23 | Please consider submitting your work for review. We are a small project, but we are super enthused when anyone comes by to help us build the best damned data grid on the internet. 24 | 25 | ## Contributing new cells 26 | 27 | If you wish to contribute new cells, please add them to the `cells` package. There are already other cells in that package which can be used as an example. If your cell editor requires additional third party dependencies please consider using a React.lazy to allow for code splitting. 28 | 29 | ## Any contributions you make will be under the MIT Software License 30 | 31 | In short, when you submit code changes, your submissions are understood to be under the same MIT License that covers the project. Feel free to contact the maintainers if that's a concern. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 typeguard, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "assumptions": { 3 | "setPublicClassFields": true, 4 | "setSpreadProperties": true 5 | }, 6 | "presets": [ 7 | [ 8 | "@babel/preset-env", 9 | { 10 | "modules": false, 11 | "targets": { 12 | "firefox": "60", 13 | "chrome": "67", 14 | "safari": "12.1" 15 | }, 16 | "exclude": ["@babel/plugin-transform-template-literals"] 17 | } 18 | ], 19 | "@babel/preset-react", 20 | "@babel/preset-typescript" 21 | ], 22 | "plugins": ["@babel/plugin-proposal-class-properties"], 23 | "comments": false, 24 | "sourceMaps": true 25 | } 26 | -------------------------------------------------------------------------------- /config/linaria.json: -------------------------------------------------------------------------------- 1 | { 2 | "classNameSlug": "gdg-[hash]" 3 | } 4 | -------------------------------------------------------------------------------- /data-grid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glideapps/glide-data-grid/96d1830be921db97d0e8a50139fea3b3ead24536/data-grid.jpg -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glideapps/glide-data-grid/96d1830be921db97d0e8a50139fea3b3ead24536/icon.png -------------------------------------------------------------------------------- /media/data-grid-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glideapps/glide-data-grid/96d1830be921db97d0e8a50139fea3b3ead24536/media/data-grid-dark.png -------------------------------------------------------------------------------- /media/data-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glideapps/glide-data-grid/96d1830be921db97d0e8a50139fea3b3ead24536/media/data-grid.png -------------------------------------------------------------------------------- /media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glideapps/glide-data-grid/96d1830be921db97d0e8a50139fea3b3ead24536/media/icon.png -------------------------------------------------------------------------------- /packages/cells/.eslintignore: -------------------------------------------------------------------------------- 1 | build.cjs 2 | node_modules/ 3 | dist/ 4 | vitest.*.ts -------------------------------------------------------------------------------- /packages/cells/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:import/recommended", 7 | "plugin:import/typescript", 8 | "plugin:react-hooks/recommended" 9 | ], 10 | "plugins": ["react", "@typescript-eslint", "import"], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "project": ["./tsconfig.json"] 14 | }, 15 | "rules": { 16 | "no-console": "warn", 17 | "guard-for-in": "error", 18 | "no-empty": "error", 19 | "no-shadow": "off", 20 | "import/no-cycle": "off", 21 | "import/namespace": "off", 22 | "import/named": "off", 23 | "import/no-unresolved": "off", // FIXME: Figure out how to make this enabled 24 | "@typescript-eslint/no-unused-vars": "off", 25 | "@typescript-eslint/explicit-function-return-type": "off", 26 | "@typescript-eslint/no-empty-interface": "off", 27 | "@typescript-eslint/no-inferrable-types": "off", 28 | "@typescript-eslint/no-explicit-any": "off", 29 | "@typescript-eslint/ban-ts-ignore": "off", 30 | "@typescript-eslint/camelcase": "off", 31 | "@typescript-eslint/interface-name-prefix": "off", 32 | "@typescript-eslint/no-floating-promises": "error", 33 | "@typescript-eslint/no-shadow": "error", 34 | "@typescript-eslint/no-misused-promises": [ 35 | "error", 36 | { 37 | "checksVoidReturn": false 38 | } 39 | ], 40 | "@typescript-eslint/ban-ts-comment": "off", 41 | "@typescript-eslint/explicit-module-boundary-types": "off", 42 | "@typescript-eslint/ban-types": "off" 43 | }, 44 | "overrides": [ 45 | { 46 | "files": ["*.ts"], 47 | "rules": { 48 | "@typescript-eslint/strict-boolean-expressions": "error" 49 | } 50 | } 51 | ], 52 | "settings": { 53 | "react": { 54 | "version": "detect" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/cells/.npmignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | .eslintrc 3 | tsconfig.json 4 | tsconfig.types.json -------------------------------------------------------------------------------- /packages/cells/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 typeguard, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/cells/README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Glide Data Grid Cells 4 |

5 |

Additional cells and features for Glide Data Grid

6 | 7 | [![Version](https://img.shields.io/npm/v/@glideapps/glide-data-grid-cells?color=blue&label=latest&style=for-the-badge)](https://github.com/glideapps/glide-data-grid/releases) 8 | [![React 16+](https://img.shields.io/badge/React-16+-00ADD8?style=for-the-badge&logo=react)](https://reactjs.org) 9 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@glideapps/glide-data-grid-cells?color=success&label=bundle&style=for-the-badge)](https://bundlephobia.com/package/@glideapps/glide-data-grid-cells) 10 | [![License](https://img.shields.io/github/license/glideapps/glide-data-grid?color=red&style=for-the-badge)](https://github.com/glideapps/glide-data-grid/blob/main/LICENSE) 11 | [![Made By Glide](https://img.shields.io/badge/❤_Made_by-Glide-11CCE5?style=for-the-badge&logo=none)](https://www.glideapps.com/jobs) 12 | 13 | ![Data Grid](https://raw.githubusercontent.com/glideapps/glide-data-grid/master/data-grid.jpg) 14 | 15 | Current cells 16 | 17 | - Star (Rating) Cell 18 | - Sparklines 19 | - Article 20 | - Dropdown 21 | - Range 22 | - User profile 23 | - Tags 24 | 25 | # Usage 26 | 27 | Step 1: Add the extra cells to your grid. 28 | 29 | ```tsx 30 | import { useExtraCells } from "@glideapps/glide-data-grid-cells"; 31 | 32 | const Grid = () => { 33 | const { customRenderers } = useExtraCells(); 34 | return ; 35 | }; 36 | ``` 37 | 38 | Step 2: Use the cells in your `getCellContent` callback 39 | 40 | ```ts 41 | import type { StarCell } from "@glideapps/glide-data-grid-cells"; 42 | 43 | const getCellContent = React.useCallback(() => { 44 | const starCell: StarCell = { 45 | kind: GridCellKind.Custom, 46 | allowOverlay: true, 47 | copyData: "4 out of 5", 48 | data: { 49 | kind: "star-cell", 50 | label: "Test", 51 | rating: 4, 52 | }, 53 | }; 54 | 55 | return starCell; 56 | }, []); 57 | ``` 58 | 59 | ## Note on ArticleCell 60 | 61 | The ArticleCell uses `@toast-ui/editor` to provide its editor. To make sure it works correctly your project will need to import the css file it depends on. 62 | 63 | ``` 64 | import "@toast-ui/editor/dist/toastui-editor.css"; 65 | ``` 66 | -------------------------------------------------------------------------------- /packages/cells/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -em 3 | source ../../config/build-util.sh 4 | 5 | ensure_bash_4 6 | 7 | shopt -s globstar 8 | 9 | echo -e "\033[0;36m🏗️ Building Glide Data Grid Cells 🏗️\033[0m" 10 | 11 | compile_esm() { 12 | compile esm true 13 | } 14 | 15 | compile_cjs() { 16 | compile cjs false 17 | } 18 | 19 | run_in_parallel compile_esm compile_cjs 20 | 21 | generate_index_css 22 | 23 | echo -e "\033[0;36m🎉 Cells Build Complete 🎉\033[0m" -------------------------------------------------------------------------------- /packages/cells/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@glideapps/glide-data-grid-cells", 3 | "version": "6.0.4-alpha9", 4 | "description": "Extra cells for glide-data-grid", 5 | "sideEffects": [ 6 | "**/*.css" 7 | ], 8 | "type": "module", 9 | "browser": "dist/esm/index.js", 10 | "main": "dist/cjs/index.js", 11 | "module": "dist/esm/index.js", 12 | "types": "dist/dts/index.d.ts", 13 | "exports": { 14 | ".": { 15 | "types": "./dist/dts/index.d.ts", 16 | "import": "./dist/esm/index.js", 17 | "require": "./dist/cjs/index.js" 18 | }, 19 | "./dist/index.css": { 20 | "import": "./dist/index.css", 21 | "require": "./dist/index.css" 22 | } 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "scripts": { 28 | "build": "./build.sh", 29 | "lint": "eslint src --ext .ts,.tsx", 30 | "test": "vitest" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/glideapps/glide-data-grid.git", 35 | "directory": "packages/cells" 36 | }, 37 | "homepage": "https://github.com/glideapps/glide-data-grid/tree/main/cells", 38 | "author": "Glide", 39 | "license": "MIT", 40 | "keywords": [ 41 | "react", 42 | "datagrid", 43 | "data-grid", 44 | "editor", 45 | "reactjs", 46 | "scrolling", 47 | "data", 48 | "table", 49 | "cell", 50 | "canvas" 51 | ], 52 | "dependencies": { 53 | "@glideapps/glide-data-grid": "6.0.4-alpha9", 54 | "@linaria/react": "^4.5.3", 55 | "@toast-ui/editor": "3.1.10", 56 | "@toast-ui/react-editor": "3.1.10", 57 | "react-select": "^5.8.0" 58 | }, 59 | "devDependencies": { 60 | "@babel/cli": "^7.16.0", 61 | "@types/prosemirror-commands": "^1.0.4", 62 | "@types/react": "16.14.21", 63 | "eslint": "^8.19.0", 64 | "eslint-plugin-import": "^2.22.0", 65 | "eslint-plugin-react": "^7.21.5", 66 | "eslint-plugin-react-hooks": "^4.2.0", 67 | "react-resize-detector": "^7.1.2", 68 | "tsc-esm-fix": "^2.7.8", 69 | "typescript": "^5.1.6" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/cells/src/cells/article-cell-types.ts: -------------------------------------------------------------------------------- 1 | import type { CustomCell } from "@glideapps/glide-data-grid"; 2 | 3 | interface ArticleCellProps { 4 | readonly kind: "article-cell"; 5 | readonly markdown: string; 6 | } 7 | 8 | export type ArticleCell = CustomCell; 9 | -------------------------------------------------------------------------------- /packages/cells/src/cells/article-cell.tsx: -------------------------------------------------------------------------------- 1 | import type { ArticleCell } from "./article-cell-types.js"; 2 | import * as React from "react"; 3 | import { type CustomRenderer, getMiddleCenterBias, GridCellKind } from "@glideapps/glide-data-grid"; 4 | 5 | const ArticleCellEditor = React.lazy(async () => await import("./article-cell-editor.js")); 6 | 7 | const renderer: CustomRenderer = { 8 | kind: GridCellKind.Custom, 9 | isMatch: (c): c is ArticleCell => (c.data as any).kind === "article-cell", 10 | draw: (args, cell) => { 11 | const { ctx, theme, rect } = args; 12 | const { markdown } = cell.data; 13 | 14 | let data = markdown; 15 | if (data.includes("\n")) { 16 | // new lines are rare and split is relatively expensive compared to the search 17 | // it pays off to not do the split contantly. 18 | data = data.split(/\r?\n/)[0]; 19 | } 20 | const max = rect.width / 4; // no need to round, slice will just truncate this 21 | if (data.length > max) { 22 | data = data.slice(0, max); 23 | } 24 | 25 | ctx.fillStyle = theme.textDark; 26 | ctx.fillText( 27 | data, 28 | rect.x + theme.cellHorizontalPadding, 29 | rect.y + rect.height / 2 + getMiddleCenterBias(ctx, theme) 30 | ); 31 | 32 | return true; 33 | }, 34 | provideEditor: () => ({ 35 | editor: p => { 36 | return ( 37 | 38 | 39 | 40 | ); 41 | }, 42 | styleOverride: { 43 | position: "fixed", 44 | left: "12.5vw", 45 | top: "12.5vh", 46 | width: "75vw", 47 | borderRadius: "9px", 48 | maxWidth: "unset", 49 | maxHeight: "unset", 50 | }, 51 | disablePadding: true, 52 | }), 53 | onPaste: (val, d) => ({ 54 | ...d, 55 | markdown: val, 56 | }), 57 | }; 58 | 59 | export default renderer; 60 | -------------------------------------------------------------------------------- /packages/cells/src/cells/spinner-cell.tsx: -------------------------------------------------------------------------------- 1 | import { type CustomCell, type CustomRenderer, GridCellKind } from "@glideapps/glide-data-grid"; 2 | 3 | interface SpinnerCellProps { 4 | readonly kind: "spinner-cell"; 5 | } 6 | 7 | export type SpinnerCell = CustomCell; 8 | 9 | const renderer: CustomRenderer = { 10 | kind: GridCellKind.Custom, 11 | isMatch: (cell: CustomCell): cell is SpinnerCell => (cell.data as any).kind === "spinner-cell", 12 | draw: args => { 13 | const { ctx, theme, rect, requestAnimationFrame } = args; 14 | 15 | const progress = (window.performance.now() % 1000) / 1000; 16 | 17 | const x = rect.x + rect.width / 2; 18 | const y = rect.y + rect.height / 2; 19 | ctx.beginPath(); 20 | ctx.arc(x, y, Math.min(12, rect.height / 6), Math.PI * 2 * progress, Math.PI * 2 * progress + Math.PI * 1.5); 21 | 22 | ctx.strokeStyle = theme.textMedium; 23 | ctx.lineWidth = 1.5; 24 | ctx.stroke(); 25 | 26 | ctx.lineWidth = 1; 27 | 28 | requestAnimationFrame(); 29 | 30 | return true; 31 | }, 32 | provideEditor: () => undefined, 33 | }; 34 | 35 | export default renderer; 36 | -------------------------------------------------------------------------------- /packages/cells/src/draw-fns.ts: -------------------------------------------------------------------------------- 1 | interface CornerRadius { 2 | tl: number; 3 | tr: number; 4 | bl: number; 5 | br: number; 6 | } 7 | 8 | export function roundedRect( 9 | ctx: CanvasRenderingContext2D, 10 | x: number, 11 | y: number, 12 | width: number, 13 | height: number, 14 | radius: number | CornerRadius 15 | ) { 16 | if (width <= 0 || height <= 0) return; 17 | if (typeof radius === "number" && radius <= 0) { 18 | ctx.rect(x, y, width, height); 19 | return; 20 | } 21 | if (typeof radius === "number") { 22 | radius = { tl: radius, tr: radius, br: radius, bl: radius }; 23 | } 24 | 25 | // restrict radius to a reasonable max 26 | radius = { 27 | tl: Math.min(radius.tl, height / 2, width / 2), 28 | tr: Math.min(radius.tr, height / 2, width / 2), 29 | bl: Math.min(radius.bl, height / 2, width / 2), 30 | br: Math.min(radius.br, height / 2, width / 2), 31 | }; 32 | 33 | radius.tl = Math.max(0, radius.tl); 34 | radius.tr = Math.max(0, radius.tr); 35 | radius.br = Math.max(0, radius.br); 36 | radius.bl = Math.max(0, radius.bl); 37 | 38 | ctx.moveTo(x + radius.tl, y); 39 | ctx.arcTo(x + width, y, x + width, y + radius.tr, radius.tr); 40 | ctx.arcTo(x + width, y + height, x + width - radius.br, y + height, radius.br); 41 | ctx.arcTo(x, y + height, x, y + height - radius.bl, radius.bl); 42 | ctx.arcTo(x, y, x + radius.tl, y, radius.tl); 43 | } 44 | -------------------------------------------------------------------------------- /packages/cells/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src/**/*.ts", "./src/**/*.tsx"], 4 | "exclude": [ 5 | "./src/**/*.stories.tsx", 6 | "./src/stories/*.tsx", 7 | "./src/docs/*.tsx", 8 | "./src/setupTests.ts", 9 | "./test/**/*.test.ts", 10 | "./test/**/*.test.tsx" 11 | ], 12 | "references": [{ "path": "../core" }], 13 | "compilerOptions": { 14 | "rootDir": "./src", 15 | "outDir": "./dist/cjs", 16 | "declarationDir": "./dist/dts", 17 | "module": "CommonJS", 18 | "verbatimModuleSyntax": false, 19 | "sourceMap": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/cells/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src/**/*.ts", "./src/**/*.tsx"], 4 | "exclude": [ 5 | "./src/**/*.stories.tsx", 6 | "./src/stories/*.tsx", 7 | "./src/docs/*.tsx", 8 | "./src/setupTests.ts", 9 | "./test/**/*.test.ts", 10 | "./test/**/*.test.tsx" 11 | ], 12 | "references": [{ "path": "../core" }], 13 | "compilerOptions": { 14 | "rootDir": "./src", 15 | "outDir": "./dist/esm", 16 | "declarationDir": "./dist/dts", 17 | "sourceMap": true, 18 | "declaration": true, 19 | "declarationMap": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/cells/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src/**/*.ts", "./src/**/*.tsx", "./test/**/*.ts", "./test/**/*.tsx"], 4 | "references": [{ "path": "../core" }] 5 | } 6 | -------------------------------------------------------------------------------- /packages/cells/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig, configDefaults } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | test: { 7 | include: ["test/**/*.test.tsx", "test/**/*.test.ts"], 8 | environment: "jsdom", 9 | setupFiles: "vitest.setup.ts", 10 | threads: false, 11 | singleThread: true, 12 | watch: false, 13 | clearMocks: true, 14 | maxConcurrency: 5, 15 | fakeTimers: { 16 | toFake: [ 17 | ...(configDefaults.fakeTimers.toFake ?? []), 18 | "performance", 19 | "requestAnimationFrame", 20 | "cancelAnimationFrame", 21 | ], 22 | }, 23 | deps: { 24 | optimizer: { 25 | web: { 26 | include: ["vitest-canvas-mock"], 27 | }, 28 | }, 29 | }, 30 | environmentOptions: { 31 | jsdom: { 32 | resources: "usable", 33 | }, 34 | }, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /packages/cells/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import "vitest-canvas-mock"; 2 | import { vi } from "vitest"; 3 | 4 | // this is needed to make the canvas mock work for some reason 5 | global.jest = vi; 6 | 7 | global.ResizeObserver = vi.fn().mockImplementation(() => ({ 8 | observe: jest.fn(), 9 | unobserve: jest.fn(), 10 | disconnect: jest.fn(), 11 | })); 12 | 13 | Image.prototype.decode = () => new Promise(resolve => window.setTimeout(resolve, 10)); 14 | -------------------------------------------------------------------------------- /packages/core/.eslintignore: -------------------------------------------------------------------------------- 1 | jest.config.js 2 | node_modules/ 3 | build.cjs 4 | vitest.*.ts -------------------------------------------------------------------------------- /packages/core/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig* 2 | coverage/* -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 typeguard, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -em 3 | source ../../config/build-util.sh 4 | 5 | ensure_bash_4 6 | 7 | shopt -s globstar 8 | 9 | echo -e "\033[0;36m🏗️ Building Glide Data Grid 🏗️\033[0m" 10 | 11 | compile_esm() { 12 | compile esm true 13 | } 14 | 15 | compile_cjs() { 16 | compile cjs false 17 | } 18 | 19 | run_in_parallel compile_esm compile_cjs 20 | 21 | generate_index_css 22 | 23 | echo -e "\033[0;36m🎉 Core Build Complete 🎉\033[0m" -------------------------------------------------------------------------------- /packages/core/src/cells/index.ts: -------------------------------------------------------------------------------- 1 | import { type InnerGridCell } from "../internal/data-grid/data-grid-types.js"; 2 | import { booleanCellRenderer } from "./boolean-cell.js"; 3 | import { bubbleCellRenderer } from "./bubble-cell.js"; 4 | import type { InternalCellRenderer } from "./cell-types.js"; 5 | import { drilldownCellRenderer } from "./drilldown-cell.js"; 6 | import { imageCellRenderer } from "./image-cell.js"; 7 | import { loadingCellRenderer } from "./loading-cell.js"; 8 | import { markdownCellRenderer } from "./markdown-cell.js"; 9 | import { markerCellRenderer } from "./marker-cell.js"; 10 | import { newRowCellRenderer } from "./new-row-cell.js"; 11 | import { numberCellRenderer } from "./number-cell.js"; 12 | import { protectedCellRenderer } from "./protected-cell.js"; 13 | import { rowIDCellRenderer } from "./row-id-cell.js"; 14 | import { textCellRenderer } from "./text-cell.js"; 15 | import { uriCellRenderer } from "./uri-cell.js"; 16 | 17 | export const AllCellRenderers = [ 18 | markerCellRenderer, 19 | newRowCellRenderer, 20 | booleanCellRenderer, 21 | bubbleCellRenderer, 22 | drilldownCellRenderer, 23 | imageCellRenderer, 24 | loadingCellRenderer, 25 | markdownCellRenderer, 26 | numberCellRenderer, 27 | protectedCellRenderer, 28 | rowIDCellRenderer, 29 | textCellRenderer, 30 | uriCellRenderer, 31 | ] as InternalCellRenderer[]; 32 | -------------------------------------------------------------------------------- /packages/core/src/cells/loading-cell.tsx: -------------------------------------------------------------------------------- 1 | import { withAlpha } from "../internal/data-grid/color-parser.js"; 2 | import { roundedRect } from "../internal/data-grid/render/data-grid-lib.js"; 3 | import { GridCellKind, type LoadingCell } from "../internal/data-grid/data-grid-types.js"; 4 | import type { InternalCellRenderer } from "./cell-types.js"; 5 | 6 | // returns a "random" number between -1 and 1 7 | function getRandomNumber(x: number, y: number): number { 8 | let seed = x * 49_632 + y * 325_176; 9 | 10 | // Inline Xorshift algorithm 11 | seed ^= seed << 13; 12 | seed ^= seed >> 17; 13 | seed ^= seed << 5; 14 | 15 | // eslint-disable-next-line unicorn/number-literal-case 16 | return (seed / 0xff_ff_ff_ff) * 2; 17 | } 18 | 19 | export const loadingCellRenderer: InternalCellRenderer = { 20 | getAccessibilityString: () => "", 21 | kind: GridCellKind.Loading, 22 | needsHover: false, 23 | useLabel: false, 24 | needsHoverPosition: false, 25 | measure: () => 120, 26 | draw: a => { 27 | const { cell, col, row, ctx, rect, theme } = a; 28 | if (cell.skeletonWidth === undefined || cell.skeletonWidth === 0) { 29 | return; 30 | } 31 | 32 | let width = cell.skeletonWidth; 33 | if (cell.skeletonWidthVariability !== undefined && cell.skeletonWidthVariability > 0) { 34 | width += Math.round(getRandomNumber(col, row) * cell.skeletonWidthVariability); 35 | } 36 | 37 | const hpad = theme.cellHorizontalPadding; 38 | if (width + hpad * 2 >= rect.width) { 39 | width = rect.width - hpad * 2 - 1; 40 | } 41 | 42 | const rectHeight = cell.skeletonHeight ?? Math.min(18, rect.height - 2 * theme.cellVerticalPadding); 43 | 44 | roundedRect( 45 | ctx, 46 | rect.x + hpad, 47 | rect.y + (rect.height - rectHeight) / 2, 48 | width, 49 | rectHeight, 50 | theme.roundingRadius ?? 3 51 | ); 52 | ctx.fillStyle = withAlpha(theme.textDark, 0.1); 53 | ctx.fill(); 54 | }, 55 | onPaste: () => undefined, 56 | }; 57 | -------------------------------------------------------------------------------- /packages/core/src/cells/markdown-cell.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import * as React from "react"; 3 | import { MarkdownOverlayEditor } from "../internal/data-grid-overlay-editor/private/markdown-overlay-editor.js"; 4 | import { drawTextCell, prepTextCell } from "../internal/data-grid/render/data-grid-lib.js"; 5 | import { GridCellKind, type MarkdownCell } from "../internal/data-grid/data-grid-types.js"; 6 | import type { InternalCellRenderer } from "./cell-types.js"; 7 | 8 | export const markdownCellRenderer: InternalCellRenderer = { 9 | getAccessibilityString: c => c.data?.toString() ?? "", 10 | kind: GridCellKind.Markdown, 11 | needsHover: false, 12 | needsHoverPosition: false, 13 | drawPrep: prepTextCell, 14 | measure: (ctx, cell, t) => { 15 | const firstLine = cell.data.split("\n")[0]; 16 | return ctx.measureText(firstLine).width + 2 * t.cellHorizontalPadding; 17 | }, 18 | draw: a => drawTextCell(a, a.cell.data, a.cell.contentAlign), 19 | onDelete: c => ({ 20 | ...c, 21 | data: "", 22 | }), 23 | provideEditor: () => p => { 24 | const { onChange, value, target, onFinishedEditing, markdownDivCreateNode, forceEditMode, validatedSelection } = 25 | p; 26 | return ( 27 | 33 | onChange({ 34 | ...value, 35 | data: e.target.value, 36 | }) 37 | } 38 | forceEditMode={forceEditMode} 39 | createNode={markdownDivCreateNode} 40 | /> 41 | ); 42 | }, 43 | onPaste: (toPaste, cell) => (toPaste === cell.data ? undefined : { ...cell, data: toPaste }), 44 | }; 45 | -------------------------------------------------------------------------------- /packages/core/src/cells/protected-cell.tsx: -------------------------------------------------------------------------------- 1 | import { degreesToRadians } from "../common/utils.js"; 2 | import { GridCellKind, type ProtectedCell } from "../internal/data-grid/data-grid-types.js"; 3 | import type { BaseDrawArgs, InternalCellRenderer } from "./cell-types.js"; 4 | 5 | export const protectedCellRenderer: InternalCellRenderer = { 6 | getAccessibilityString: () => "", 7 | measure: () => 108, 8 | kind: GridCellKind.Protected, 9 | needsHover: false, 10 | needsHoverPosition: false, 11 | draw: drawProtectedCell, 12 | onPaste: () => undefined, 13 | }; 14 | 15 | function drawProtectedCell(args: BaseDrawArgs) { 16 | const { ctx, theme, rect } = args; 17 | const { x, y, height: h } = rect; 18 | 19 | ctx.beginPath(); 20 | 21 | const radius = 2.5; 22 | let xStart = x + theme.cellHorizontalPadding + radius; 23 | const center = y + h / 2; 24 | const p = Math.cos(degreesToRadians(30)) * radius; 25 | const q = Math.sin(degreesToRadians(30)) * radius; 26 | 27 | for (let i = 0; i < 12; i++) { 28 | ctx.moveTo(xStart, center - radius); 29 | ctx.lineTo(xStart, center + radius); 30 | 31 | ctx.moveTo(xStart + p, center - q); 32 | ctx.lineTo(xStart - p, center + q); 33 | 34 | ctx.moveTo(xStart - p, center - q); 35 | ctx.lineTo(xStart + p, center + q); 36 | xStart += 8; 37 | } 38 | ctx.lineWidth = 1.1; 39 | ctx.lineCap = "square"; 40 | ctx.strokeStyle = theme.textLight; 41 | ctx.stroke(); 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/src/cells/row-id-cell.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GrowingEntry } from "../internal/growing-entry/growing-entry.js"; 3 | import { drawTextCell, prepTextCell } from "../internal/data-grid/render/data-grid-lib.js"; 4 | import { GridCellKind, type RowIDCell } from "../internal/data-grid/data-grid-types.js"; 5 | import type { InternalCellRenderer } from "./cell-types.js"; 6 | 7 | export const rowIDCellRenderer: InternalCellRenderer = { 8 | getAccessibilityString: c => c.data?.toString() ?? "", 9 | kind: GridCellKind.RowID, 10 | needsHover: false, 11 | needsHoverPosition: false, 12 | drawPrep: (a, b) => prepTextCell(a, b, a.theme.textLight), 13 | draw: a => drawTextCell(a, a.cell.data, a.cell.contentAlign), 14 | measure: (ctx, cell, theme) => ctx.measureText(cell.data).width + theme.cellHorizontalPadding * 2, 15 | // eslint-disable-next-line react/display-name 16 | provideEditor: () => p => { 17 | const { isHighlighted, onChange, value, validatedSelection } = p; 18 | return ( 19 | 26 | onChange({ 27 | ...value, 28 | data: e.target.value, 29 | }) 30 | } 31 | /> 32 | ); 33 | }, 34 | onPaste: () => undefined, 35 | }; 36 | -------------------------------------------------------------------------------- /packages/core/src/common/browser-detect.ts: -------------------------------------------------------------------------------- 1 | class Lazy { 2 | private fn: () => T; 3 | private val: T | undefined; 4 | constructor(fn: () => T) { 5 | this.fn = fn; 6 | } 7 | 8 | public get value() { 9 | return this.val ?? (this.val = this.fn()); 10 | } 11 | } 12 | 13 | function lazy(fn: () => T) { 14 | return new Lazy(fn); 15 | } 16 | 17 | // next.js apps don't have window available at import time, so this will fail if its not lazy. 18 | export const browserIsFirefox = lazy(() => window.navigator.userAgent.includes("Firefox")); 19 | export const browserIsSafari = lazy( 20 | () => 21 | window.navigator.userAgent.includes("Mac OS") && 22 | window.navigator.userAgent.includes("Safari") && 23 | !window.navigator.userAgent.includes("Chrome") 24 | ); 25 | export const browserIsOSX = lazy(() => window.navigator.platform.toLowerCase().startsWith("mac")); 26 | -------------------------------------------------------------------------------- /packages/core/src/common/resize-detector.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/strict-boolean-expressions */ 2 | import { useLayoutEffect, useState, useRef, type MutableRefObject } from "react"; 3 | interface ReactResizeDetectorDimensions { 4 | height?: number; 5 | width?: number; 6 | } 7 | 8 | export function useResizeDetector( 9 | initialSize?: readonly [width: number, height: number] 10 | ): UseResizeDetectorReturn { 11 | const ref = useRef(null); 12 | 13 | const [size, setSize] = useState({ 14 | width: initialSize?.[0], 15 | height: initialSize?.[1], 16 | }); 17 | 18 | useLayoutEffect(() => { 19 | const resizeCallback: ResizeObserverCallback = entries => { 20 | for (const entry of entries) { 21 | const { width, height } = (entry && entry.contentRect) || {}; 22 | setSize(cv => (cv.width === width && cv.height === height ? cv : { width, height })); 23 | } 24 | }; 25 | 26 | const resizeObserver = new window.ResizeObserver(resizeCallback); 27 | 28 | if (ref.current) { 29 | resizeObserver.observe(ref.current, undefined); 30 | } 31 | 32 | return () => { 33 | resizeObserver.disconnect(); 34 | }; 35 | // eslint-disable-next-line react-hooks/exhaustive-deps 36 | }, [ref.current]); 37 | 38 | return { ref, ...size }; 39 | } 40 | 41 | export interface UseResizeDetectorReturn extends ReactResizeDetectorDimensions { 42 | ref: MutableRefObject; 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/data-editor-all.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { DataEditor, type DataEditorProps, type DataEditorRef } from "./data-editor/data-editor.js"; 3 | import { AllCellRenderers } from "./cells/index.js"; 4 | import { sprites } from "./internal/data-grid/sprites.js"; 5 | import ImageWindowLoaderImpl from "./common/image-window-loader.js"; 6 | import type { ImageWindowLoader } from "./internal/data-grid/image-window-loader-interface.js"; 7 | 8 | export interface DataEditorAllProps extends Omit { 9 | imageWindowLoader?: ImageWindowLoader; 10 | } 11 | 12 | const DataEditorAllImpl: React.ForwardRefRenderFunction = (p, ref) => { 13 | const allSprites = React.useMemo(() => { 14 | return { ...sprites, ...p.headerIcons }; 15 | }, [p.headerIcons]); 16 | 17 | const imageWindowLoader = React.useMemo(() => { 18 | return p.imageWindowLoader ?? new ImageWindowLoaderImpl(); 19 | }, [p.imageWindowLoader]); 20 | 21 | return ( 22 | 29 | ); 30 | }; 31 | 32 | export const DataEditorAll = React.forwardRef(DataEditorAllImpl); 33 | -------------------------------------------------------------------------------- /packages/core/src/data-editor/use-autoscroll.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { GridMouseCellEventArgs } from "../internal/data-grid/event-args.js"; 3 | 4 | const maxPxPerMs = 2; 5 | const msToFullSpeed = 1300; 6 | 7 | export function useAutoscroll( 8 | scrollDirection: GridMouseCellEventArgs["scrollEdge"] | undefined, 9 | scrollRef: React.MutableRefObject, 10 | onScroll?: () => void 11 | ) { 12 | const speedScalar = React.useRef(0); 13 | const [xDir, yDir] = scrollDirection ?? [0, 0]; 14 | React.useEffect(() => { 15 | if (xDir === 0 && yDir === 0) { 16 | speedScalar.current = 0; 17 | return; 18 | } 19 | let cancelled = false; 20 | 21 | let lastTime = 0; 22 | const scrollFn = (curTime: number) => { 23 | if (cancelled) return; 24 | if (lastTime === 0) { 25 | lastTime = curTime; 26 | } else { 27 | const step = curTime - lastTime; 28 | speedScalar.current = Math.min(1, speedScalar.current + step / msToFullSpeed); 29 | const motion = speedScalar.current ** 1.618 * step * maxPxPerMs; 30 | scrollRef.current?.scrollBy(xDir * motion, yDir * motion); 31 | lastTime = curTime; 32 | onScroll?.(); 33 | } 34 | window.requestAnimationFrame(scrollFn); 35 | }; 36 | window.requestAnimationFrame(scrollFn); 37 | return () => { 38 | cancelled = true; 39 | }; 40 | }, [scrollRef, xDir, yDir, onScroll]); 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/data-editor/use-rem-adjuster.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getDataEditorTheme, type Theme } from "../common/styles.js"; 3 | 4 | interface DataEditorDimensions { 5 | rowHeight: number | ((n: number) => number); 6 | headerHeight: number; 7 | groupHeaderHeight: number; 8 | theme: Partial | undefined; 9 | overscrollX: number | undefined; 10 | overscrollY: number | undefined; 11 | } 12 | 13 | interface DataEditorProps { 14 | rowHeight: number | ((n: number) => number); 15 | headerHeight: number; 16 | groupHeaderHeight: number; 17 | theme?: Partial; 18 | overscrollX?: number; 19 | overscrollY?: number; 20 | scaleToRem: boolean; 21 | remSize: number; 22 | } 23 | 24 | export function useRemAdjuster({ 25 | rowHeight: rowHeightIn, 26 | headerHeight: headerHeightIn, 27 | groupHeaderHeight: groupHeaderHeightIn, 28 | theme: themeIn, 29 | overscrollX: overscrollXIn, 30 | overscrollY: overscrollYIn, 31 | scaleToRem, 32 | remSize, 33 | }: DataEditorProps): DataEditorDimensions { 34 | const [rowHeight, headerHeight, groupHeaderHeight, theme, overscrollX, overscrollY] = React.useMemo(() => { 35 | if (!scaleToRem || remSize === 16) 36 | return [rowHeightIn, headerHeightIn, groupHeaderHeightIn, themeIn, overscrollXIn, overscrollYIn]; 37 | const scaler = remSize / 16; 38 | const rh = rowHeightIn; 39 | const bt = getDataEditorTheme(); 40 | return [ 41 | typeof rh === "number" ? rh * scaler : (n: number) => Math.ceil(rh(n) * scaler), 42 | Math.ceil(headerHeightIn * scaler), 43 | Math.ceil(groupHeaderHeightIn * scaler), 44 | { 45 | ...themeIn, 46 | headerIconSize: (themeIn?.headerIconSize ?? bt.headerIconSize) * scaler, 47 | cellHorizontalPadding: (themeIn?.cellHorizontalPadding ?? bt.cellHorizontalPadding) * scaler, 48 | cellVerticalPadding: (themeIn?.cellVerticalPadding ?? bt.cellVerticalPadding) * scaler, 49 | }, 50 | Math.ceil((overscrollXIn ?? 0) * scaler), 51 | Math.ceil((overscrollYIn ?? 0) * scaler), 52 | ]; 53 | }, [groupHeaderHeightIn, headerHeightIn, overscrollXIn, overscrollYIn, remSize, rowHeightIn, scaleToRem, themeIn]); 54 | 55 | return { rowHeight, headerHeight, groupHeaderHeight, theme, overscrollX, overscrollY }; 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/src/data-editor/visible-region.ts: -------------------------------------------------------------------------------- 1 | import { type Rectangle, type Item } from "../internal/data-grid/data-grid-types.js"; 2 | 3 | export type VisibleRegion = Rectangle & { 4 | /** value in px */ 5 | tx?: number; 6 | /** value in px */ 7 | ty?: number; 8 | extras?: { 9 | selected?: Item; 10 | /** 11 | * @deprecated 12 | */ 13 | freezeRegion?: Rectangle; 14 | 15 | /** 16 | * All visible freeze regions 17 | */ 18 | freezeRegions?: readonly Rectangle[]; 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/core/src/docs/00-faq.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { SimpleThemeWrapper } from "../stories/story-utils.js"; 4 | import { DocWrapper, Marked } from "./doc-wrapper.js"; 5 | 6 | export default { 7 | title: "Glide-Data-Grid/Docs", 8 | decorators: [ 9 | (Story: React.ComponentType) => ( 10 | 11 | 12 | 13 | ), 14 | ], 15 | }; 16 | 17 | export const FAQ: React.VFC = () => { 18 | return ( 19 | 20 | 21 | {` 22 | # FAQ 23 | 24 | ### Nothing shows up? It crashes when I edit a cell? 25 | 26 | Please read the [Prerequisites section in the docs](https://github.com/glideapps/glide-data-grid/blob/main/packages/core/API.md). 27 | 28 | ### Does it work with screen readers and other a11y tools? 29 | 30 | Yes. Unfortunately none of the primary developers are accessibility users so there are likely flaws in the implementation we are not aware of. Bug reports welcome! 31 | 32 | ### Does it support my data source? 33 | 34 | Yes. 35 | 36 | Data Grid is agnostic about the way you load/store/generate/mutate your data. What it requires is that you tell it which columns you have, how many rows, and to give it a function it can call to get the data for a cell in a specific row and column. 37 | 38 | ### Does it do sorting? 39 | 40 | Yes through the [glide-data-grid-source](https://www.npmjs.com/package/@glideapps/glide-data-grid-source) package. 41 | 42 | ### Does it do search? 43 | 44 | Yes, built in! There are examples in the storybook. 45 | 46 | ### Can it filter? 47 | 48 | Nothing built in yet. It is planned for the \`glide-data-grid-source\`. 49 | 50 | ### Can it do frozen columns? 51 | 52 | Yes 53 | 54 | ### Can I render my own cells? 55 | 56 | Yes 57 | 58 | `} 59 | 60 | 61 | ); 62 | }; 63 | (FAQ as any).storyName = "00. FAQ"; 64 | -------------------------------------------------------------------------------- /packages/core/src/docs/examples/add-column.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DataEditorAll as DataEditor } from "../../data-editor-all.js"; 3 | import { 4 | BeautifulWrapper, 5 | Description, 6 | MoreInfo, 7 | useMockDataGenerator, 8 | defaultProps, 9 | } from "../../data-editor/stories/utils.js"; 10 | import { SimpleThemeWrapper } from "../../stories/story-utils.js"; 11 | 12 | export default { 13 | title: "Glide-Data-Grid/DataEditor Demos", 14 | 15 | decorators: [ 16 | (Story: React.ComponentType) => ( 17 | 18 | 22 | You can add and remove columns at your disposal 23 | Use the story's controls to change the number of columns 24 | 25 | }> 26 | 27 | 28 | 29 | ), 30 | ], 31 | }; 32 | 33 | interface AddColumnsProps { 34 | columnsCount: number; 35 | } 36 | 37 | export const AddColumns: React.FC = p => { 38 | const { cols, getCellContent } = useMockDataGenerator(p.columnsCount); 39 | 40 | return ( 41 | 49 | ); 50 | }; 51 | (AddColumns as any).args = { 52 | columnsCount: 10, 53 | }; 54 | (AddColumns as any).argTypes = { 55 | columnsCount: { 56 | control: { 57 | type: "range", 58 | min: 2, 59 | max: 200, 60 | }, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /packages/core/src/docs/examples/add-data-to-top.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DataEditorAll as DataEditor } from "../../data-editor-all.js"; 3 | import { 4 | BeautifulWrapper, 5 | Description, 6 | useMockDataGenerator, 7 | defaultProps, 8 | clearCell, 9 | } from "../../data-editor/stories/utils.js"; 10 | import { SimpleThemeWrapper } from "../../stories/story-utils.js"; 11 | 12 | export default { 13 | title: "Glide-Data-Grid/DataEditor Demos", 14 | 15 | decorators: [ 16 | (Story: React.ComponentType) => ( 17 | 18 | 22 | 23 | You can return a different location to have the new row append take place. 24 | 25 | 26 | }> 27 | 28 | 29 | 30 | ), 31 | ], 32 | }; 33 | 34 | export const AddDataToTop: React.VFC = () => { 35 | const { cols, getCellContent, setCellValueRaw, setCellValue } = useMockDataGenerator(60, false); 36 | 37 | const [numRows, setNumRows] = React.useState(50); 38 | 39 | const onRowAppended = React.useCallback(async () => { 40 | // shift all of the existing cells down 41 | for (let y = numRows; y > 0; y--) { 42 | for (let x = 0; x < 6; x++) { 43 | setCellValueRaw([x, y], getCellContent([x, y - 1])); 44 | } 45 | } 46 | for (let c = 0; c < 6; c++) { 47 | const cell = getCellContent([c, 0]); 48 | setCellValueRaw([c, 0], clearCell(cell)); 49 | } 50 | setNumRows(cv => cv + 1); 51 | return "top" as const; 52 | }, [getCellContent, numRows, setCellValueRaw]); 53 | 54 | return ( 55 | 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /packages/core/src/docs/examples/all-cell-kinds.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DataEditorAll as DataEditor } from "../../data-editor-all.js"; 3 | import { 4 | BeautifulWrapper, 5 | Description, 6 | PropName, 7 | defaultProps, 8 | useAllMockedKinds, 9 | } from "../../data-editor/stories/utils.js"; 10 | import { SimpleThemeWrapper } from "../../stories/story-utils.js"; 11 | 12 | export default { 13 | title: "Glide-Data-Grid/DataEditor Demos", 14 | 15 | decorators: [ 16 | (Story: React.ComponentType) => ( 17 | 18 | 22 | Data grid supports plenty cell kinds. Anything under GridCellKind. 23 | 24 | }> 25 | 26 | 27 | 28 | ), 29 | ], 30 | }; 31 | 32 | export const AllCellKinds: React.VFC = () => { 33 | const { cols, getCellContent, onColumnResize, setCellValue } = useAllMockedKinds(); 34 | 35 | return ( 36 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/core/src/docs/examples/automatic-row-markers.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DataEditorAll as DataEditor } from "../../data-editor-all.js"; 3 | import { 4 | BeautifulWrapper, 5 | Description, 6 | MoreInfo, 7 | PropName, 8 | useMockDataGenerator, 9 | KeyName, 10 | defaultProps, 11 | } from "../../data-editor/stories/utils.js"; 12 | import { SimpleThemeWrapper } from "../../stories/story-utils.js"; 13 | 14 | export default { 15 | title: "Glide-Data-Grid/DataEditor Demos", 16 | 17 | decorators: [ 18 | (Story: React.ComponentType) => ( 19 | 20 | 21 | 22 | ), 23 | ], 24 | }; 25 | 26 | export const AutomaticRowMarkers: React.VFC = () => { 27 | const { cols, getCellContent } = useMockDataGenerator(6); 28 | 29 | const dataEditor = ( 30 | 37 | ); 38 | 39 | return ( 40 | 44 | 45 | You can enable row markers with rich selection behavior using the{" "} 46 | rowMarkers prop. 47 | 48 | 49 | Use + click to make range selections, and Ctrl ( 50 | on Mac) + click to add/remove individual rows. 51 | 52 | 53 | }> 54 | {dataEditor} 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/core/src/docs/examples/column-groups.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DataEditorAll as DataEditor } from "../../data-editor-all.js"; 3 | import { 4 | BeautifulWrapper, 5 | Description, 6 | PropName, 7 | useMockDataGenerator, 8 | defaultProps, 9 | } from "../../data-editor/stories/utils.js"; 10 | import { GridColumnIcon } from "../../internal/data-grid/data-grid-types.js"; 11 | import { SimpleThemeWrapper } from "../../stories/story-utils.js"; 12 | 13 | export default { 14 | title: "Glide-Data-Grid/DataEditor Demos", 15 | 16 | decorators: [ 17 | (Story: React.ComponentType) => ( 18 | 19 | 23 | Columns in the data grid may be grouped by setting their group{" "} 24 | property. 25 | 26 | }> 27 | 28 | 29 | 30 | ), 31 | ], 32 | }; 33 | 34 | export const ColumnGroups: React.VFC = () => { 35 | const { cols, getCellContent } = useMockDataGenerator(20, true, true); 36 | 37 | return ( 38 | window.alert(`Please rename group ${x} to ${y}`)} 42 | columns={cols} 43 | rows={1000} 44 | getGroupDetails={g => ({ 45 | name: g, 46 | icon: g === "" ? undefined : GridColumnIcon.HeaderCode, 47 | })} 48 | rowMarkers="both" 49 | /> 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/core/src/docs/examples/content-alignment.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DataEditorAll as DataEditor } from "../../data-editor-all.js"; 3 | import { 4 | BeautifulWrapper, 5 | Description, 6 | PropName, 7 | defaultProps, 8 | useAllMockedKinds, 9 | } from "../../data-editor/stories/utils.js"; 10 | import { SimpleThemeWrapper } from "../../stories/story-utils.js"; 11 | 12 | export default { 13 | title: "Glide-Data-Grid/DataEditor Demos", 14 | 15 | decorators: [ 16 | (Story: React.ComponentType) => ( 17 | 18 | 22 | You can customize the content alignment by setting contentAlign of a 23 | cell to left, right or center 24 | . 25 | 26 | }> 27 | 28 | 29 | 30 | ), 31 | ], 32 | }; 33 | 34 | export const ContentAlignment: React.VFC = () => { 35 | const { cols, getCellContent } = useAllMockedKinds(); 36 | 37 | const mangledGetCellContent = React.useCallback( 38 | cell => { 39 | const [col, _row] = cell; 40 | if (col === 3) { 41 | return { 42 | ...getCellContent(cell), 43 | contentAlign: "center", 44 | }; 45 | } 46 | if (col === 4) { 47 | return { 48 | ...getCellContent(cell), 49 | contentAlign: "right", 50 | }; 51 | } 52 | if (col === 5) { 53 | return { 54 | ...getCellContent(cell), 55 | contentAlign: "left", 56 | }; 57 | } 58 | return getCellContent(cell); 59 | }, 60 | [getCellContent] 61 | ); 62 | 63 | return ; 64 | }; 65 | -------------------------------------------------------------------------------- /packages/core/src/docs/examples/copy-support.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DataEditorAll as DataEditor } from "../../data-editor-all.js"; 3 | import { 4 | BeautifulWrapper, 5 | Description, 6 | MoreInfo, 7 | PropName, 8 | useMockDataGenerator, 9 | defaultProps, 10 | } from "../../data-editor/stories/utils.js"; 11 | import { SimpleThemeWrapper } from "../../stories/story-utils.js"; 12 | 13 | export default { 14 | title: "Glide-Data-Grid/DataEditor Demos", 15 | 16 | decorators: [ 17 | (Story: React.ComponentType) => ( 18 | 19 | 23 | 24 | Large amounts of data can be copied and customized using{" "} 25 | getCellsForSelection. 26 | 27 | 28 | The data is copied into a format ready to be pasted into Excel or Google Sheets 29 | 30 |