├── .babelrc.js ├── .browserslistrc ├── .eslintignore ├── .eslintrc.json ├── .flowconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── changelog-configuration.json ├── labeler.yml ├── release.yml └── workflows │ ├── changelog.yml │ ├── gh-pages.yml │ ├── label.yml │ ├── lint.yml │ ├── packj.yml │ ├── release.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmignore ├── .packj.yaml ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── __mocks__ └── react-grid-layout.js ├── css └── styles.css ├── dist ├── react-grid-layout.min.js └── react-grid-layout.min.js.map ├── examples └── util │ ├── example-styles.css │ ├── generate.js │ ├── template.ejs │ └── vars.js ├── flow-typed └── npm │ ├── clsx_v1.x.x.js │ ├── enzyme_v3.x.x.js │ ├── jest_v24.x.x.js │ └── prop-types_v15.x.x.js ├── index-dev.html ├── index-dev.js ├── index.html ├── index.js ├── index.js.flow ├── lib ├── GridItem.jsx ├── ReactGridLayout.jsx ├── ReactGridLayoutPropTypes.js ├── ResponsiveReactGridLayout.jsx ├── calculateUtils.js ├── components │ └── WidthProvider.jsx ├── fastRGLPropsEqual.js ├── responsiveUtils.js └── utils.js ├── margin.png ├── package.json ├── test ├── dev-hook.jsx ├── examples │ ├── .eslintrc.json │ ├── 00-showcase.jsx │ ├── 01-basic.jsx │ ├── 02-no-dragging.jsx │ ├── 03-messy.jsx │ ├── 04-grid-property.jsx │ ├── 05-static-elements.jsx │ ├── 06-dynamic-add-remove.jsx │ ├── 07-localstorage.jsx │ ├── 08-localstorage-responsive.jsx │ ├── 09-min-max-wh.jsx │ ├── 10-dynamic-min-max-wh.jsx │ ├── 11-no-vertical-compact.jsx │ ├── 12-prevent-collision.jsx │ ├── 13-error-case.jsx │ ├── 14-toolbox.jsx │ ├── 15-drag-from-outside.jsx │ ├── 16-bounded.jsx │ ├── 17-responsive-bootstrap-style.jsx │ ├── 18-scale.jsx │ ├── 19-allow-overlap.jsx │ ├── 20-resizable-handles.jsx │ └── 21-horizontal.jsx ├── spec │ ├── __snapshots__ │ │ └── lifecycle-test.js.snap │ ├── lifecycle-test.js │ └── utils-test.js ├── test-hook.jsx └── util │ ├── deepFreeze.js │ └── setupTests.js ├── webpack-dev-server.config.js ├── webpack-examples.config.js ├── webpack.config.js └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const es6Compat = 4 | process.env.BABEL_ES_COMPAT === "6" || process.env.NODE_ENV === "test"; 5 | 6 | module.exports = { 7 | presets: [ 8 | [ 9 | "@babel/preset-env", 10 | { 11 | targets: es6Compat ? "maintained node versions" : "> 0.25%, not dead" 12 | } 13 | ], 14 | "@babel/react", 15 | "@babel/preset-flow" 16 | ], 17 | plugins: [ 18 | "@babel/plugin-transform-flow-comments", 19 | "@babel/plugin-proposal-class-properties", 20 | "babel-plugin-preval" 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.25% 2 | ie 11 3 | not dead -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | build 3 | dist 4 | flow-typed -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@babel/eslint-parser", 4 | "plugins": ["react", "flowtype", "unicorn"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:react/recommended", 8 | "plugin:flowtype/recommended" 9 | ], 10 | "rules": { 11 | "no-console": "off", 12 | "no-use-before-define": ["error", "nofunc"], 13 | "no-unused-vars": [ 14 | "warn", 15 | { 16 | "argsIgnorePattern": "^(e|_.*)$", 17 | "vars": "local", 18 | "varsIgnorePattern": "(debug|^_)" 19 | } 20 | ], 21 | "prefer-const": "error", 22 | "react/jsx-boolean-value": ["error", "always"], 23 | "unicorn/better-regex": "warn", 24 | "unicorn/expiring-todo-comments": "error", 25 | "unicorn/no-abusive-eslint-disable": "error" 26 | }, 27 | "env": { 28 | "browser": true, 29 | "node": true, 30 | "es6": true 31 | }, 32 | "settings": { 33 | "react": { 34 | "version": "detect" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [version] 2 | ^0.172.0 3 | 4 | [ignore] 5 | /build/.* 6 | /dist/.* 7 | /node_modules/resolve/.* 8 | /node_modules/.*/resolve/.* 9 | # Speeds parsing: 10 | .*/node_modules/@babel.* 11 | .*/node_modules/babel.* 12 | 13 | [include] 14 | 15 | [libs] 16 | flow-typed/ 17 | 18 | [options] 19 | emoji=true 20 | module.use_strict=true 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "🐛 Bug report" 2 | description: Create a report to help us improve 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thank you for reporting an issue :pray:. 8 | 9 | This issue tracker is for reporting bugs found in `react-grid-layout` (https://github.com/react-grid-layout/react-grid-layout). 10 | If you have a question about how to achieve something and are struggling, please post a question 11 | inside of `react-grid-layout` Discussions tab: https://github.com/react-grid-layout/react-grid-layout/discussions 12 | 13 | Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already: 14 | - `react-grid-layout` Issues tab: https://github.com/react-grid-layout/react-grid-layout/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc 15 | - `react-grid-layout` closed issues tab: https://github.com/react-grid-layout/react-grid-layout/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed 16 | - `react-grid-layout` Discussions tab: https://github.com/react-grid-layout/react-grid-layout/discussions 17 | 18 | The more information you fill in, the better the community can help you. 19 | - type: textarea 20 | id: description 21 | attributes: 22 | label: Describe the bug 23 | description: Provide a clear and concise description of the challenge you are running into. 24 | validations: 25 | required: true 26 | - type: input 27 | id: link 28 | attributes: 29 | label: Your Example Website or App 30 | description: | 31 | Which website or app were you using when the bug happened? 32 | Note: 33 | - Your bug will may get fixed much faster if we can run your code and it doesn't have dependencies other than the `react-grid-layout` npm package. 34 | - To create a shareable code example, consider using our pre-made CodeSandbox Template or create your own: https://codesandbox.io/s/staging-bush-3lvt7?file=/src/ShowcaseLayout.js) 35 | - Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve. 36 | placeholder: | 37 | e.g. https://codesandbox.io/s/...... OR Github Repo 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: steps 42 | attributes: 43 | label: Steps to Reproduce the Bug or Issue 44 | description: Describe the steps we have to take to reproduce the behavior. 45 | placeholder: | 46 | 1. Go to '...' 47 | 2. Click on '....' 48 | 3. Scroll down to '....' 49 | 4. See error 50 | validations: 51 | required: true 52 | - type: textarea 53 | id: expected 54 | attributes: 55 | label: Expected behavior 56 | description: Provide a clear and concise description of what you expected to happen. 57 | placeholder: | 58 | As a user, I expected ___ behavior but i am seeing ___ 59 | validations: 60 | required: true 61 | - type: input 62 | id: library_version 63 | attributes: 64 | label: react-grid-layout library version 65 | description: What version of the `react-grid-layout` library are you using? 66 | validations: 67 | required: true 68 | - type: input 69 | id: os 70 | attributes: 71 | label: Operating System Version 72 | description: What operating system are you using? 73 | placeholder: | 74 | - OS: [e.g. macOS, Windows, Linux] 75 | validations: 76 | required: true 77 | - type: dropdown 78 | id: browser_type 79 | attributes: 80 | label: Browser 81 | description: Select the browsers where the issue can be reproduced (that you know of). 82 | options: 83 | - "Chrome" 84 | - "Firefox" 85 | - "Safari" 86 | - "Edge" 87 | - "Opera" 88 | - "Other (add additonal context)" 89 | validations: 90 | required: true 91 | - type: textarea 92 | id: additional 93 | attributes: 94 | label: Additional context 95 | description: Add any other context about the problem here. 96 | - type: textarea 97 | id: screenshots_or_videos 98 | attributes: 99 | label: Screenshots or Videos 100 | description: | 101 | If applicable, add screenshots or a video to help explain your problem. 102 | For more information on the supported file image/file types and the file size limits, please refer 103 | to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files 104 | placeholder: | 105 | You can drag your video or image files inside of this editor ↓ 106 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🤔 Long question or idea? 4 | url: https://github.com/react-grid-layout/react-grid-layout/discussions 5 | about: Ask long-form questions and discuss ideas. 6 | - name: 👥 Community Code Examples for `react-grid-layout` 7 | url: https://codesandbox.io/search?refinementList%5Bnpm_dependencies.dependency%5D%5B0%5D=react-grid-layout&refinementList%5Btemplate%5D%5B0%5D=create-react-app&refinementList%5Btemplate%5D%5B1%5D=create-react-app-typescript&page=1&configure%5BhitsPerPage%5D=12 8 | about: See `react-grid-layout` examples created by our community of users 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 6 | 7 | This PR [adds/removes/fixes/replaces] the [feature/bug/etc]. 8 | 9 | ## What type of PR is this? (check all applicable) 10 | 11 | - [ ] 🍕 Feature 12 | - [ ] 🐛 Bug Fix 13 | - [ ] 📝 Documentation Update 14 | - [ ] 🎨 Style 15 | - [ ] 🧑‍💻 Code Refactor 16 | - [ ] 🔥 Performance Improvements 17 | - [ ] ✅ Test 18 | - [ ] 🤖 Build 19 | - [ ] 🔁 CI 20 | - [ ] 📦 Chore (Release) 21 | - [ ] ⏩ Revert 22 | 23 | ## Related Tickets & Documents 24 | 25 | 29 | 30 | ## Mobile & Desktop Screenshots/Recordings 31 | 32 | 33 | 34 | ## Added tests? 35 | 36 | - [ ] 👍 yes 37 | - [ ] 🙅 no, because they aren't needed 38 | - [ ] 🙋 no, because I need help 39 | 40 | ## Added to documentation? 41 | 42 | - [ ] 📜 README.md 43 | - [ ] 📓 examples 44 | - [ ] 🙅 no documentation needed 45 | 46 | 52 | 53 | 68 | -------------------------------------------------------------------------------- /.github/changelog-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | { 4 | "title": "## 🚀 Features", 5 | "labels": ["feature"] 6 | }, 7 | { 8 | "title": "## 🐛 Fixes", 9 | "labels": ["fix"] 10 | }, 11 | { 12 | "title": "## 🧪 Tests", 13 | "labels": ["test"] 14 | } 15 | ], 16 | "ignore_labels": ["ignore_changelog"], 17 | "sort": "ASC", 18 | "template": "${{CHANGELOG}}\n\n
\nUncategorized\n\n${{UNCATEGORIZED}}\n
", 19 | "pr_template": "- ${{TITLE}}\n - PR: #${{NUMBER}}", 20 | "empty_template": "- no changes", 21 | "label_extractor": [ 22 | { 23 | "pattern": "\\[Issue\\]", 24 | "on_property": "title", 25 | "method": "match" 26 | } 27 | ], 28 | "transformers": [], 29 | "max_tags_to_fetch": 200, 30 | "max_pull_requests": 200, 31 | "max_back_track_time_days": 365, 32 | "exclude_merge_branches": [], 33 | "tag_resolver": {}, 34 | "base_branches": [] 35 | } 36 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | documentation: 2 | - changed-files: 3 | - any-glob-to-any-file: 4 | - test/examples/**/* 5 | - README.md 6 | - examples/**/* 7 | 8 | core: 9 | - changed-files: 10 | - any-glob-to-any-file: 11 | - lib/**/* 12 | 13 | release: 14 | - changed-files: 15 | - any-glob-to-any-file: 16 | - dist/**/* 17 | 18 | tests: 19 | - changed-files: 20 | - any-glob-to-any-file: 21 | - test/spec/**/* 22 | 23 | deps: 24 | - changed-files: 25 | - any-glob-to-any-file: 26 | - yarn.lock 27 | 28 | infrastructure: 29 | - changed-files: 30 | - any-glob-to-any-file: 31 | - .github/**/* 32 | - .babelrc 33 | - .eslintignore 34 | - .eslintrc.json 35 | - .flowconfig 36 | - .gitignore 37 | - .npmignore 38 | - .travis.yml 39 | - build.sh 40 | - package.json 41 | - webpack* 42 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | - dependencies 6 | authors: 7 | - octocat 8 | categories: 9 | - title: Breaking Changes 🛠 10 | labels: 11 | - Semver-Major 12 | - breaking-change 13 | - title: Exciting New Features 🎉 14 | labels: 15 | - Semver-Minor 16 | - enhancement 17 | - title: Other Changes 18 | labels: 19 | - "*" 20 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: "changelog" 2 | permissions: 3 | contents: write 4 | on: 5 | push: 6 | tags: 7 | - "*" 8 | 9 | jobs: 10 | release: 11 | if: startsWith(github.ref, 'refs/tags/') 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Build Changelog 18 | id: github_release 19 | uses: mikepenz/release-changelog-builder-action@v2 20 | with: 21 | configuration: ".github/changelog-configuration.json" 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | - name: Create Release 26 | uses: actions/create-release@v1 27 | with: 28 | tag_name: ${{ github.ref }} 29 | release_name: ${{ github.ref }} 30 | body: ${{steps.github_release.outputs.changelog}} 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy to GitHub Pages 2 | permissions: 3 | contents: write 4 | on: 5 | push: 6 | tags: 7 | - "*" 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 🛎️ 13 | uses: actions/checkout@v4 14 | 15 | - name: Install and Build 16 | run: | 17 | yarn 18 | make build-example 19 | 20 | - name: Deploy 🚀 21 | uses: JamesIves/github-pages-deploy-action@v4 22 | with: 23 | branch: gh-pages # The branch the action should deploy to. 24 | folder: examples # The folder the action should deploy. 25 | target-folder: examples # The destination. Shouldn't touch other folders. 26 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull request labeler 3 | on: 4 | - pull_request_target 5 | jobs: 6 | labeler: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | pull-requests: write 11 | steps: 12 | - id: label-the-PR 13 | uses: actions/labeler@v5 14 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | # Trigger the workflow on push or pull request, 5 | # but only for the master branch 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | run-linters: 15 | name: Run linters 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Check out Git repository 20 | uses: actions/checkout@v2 21 | 22 | - name: Set up Node.js 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: 18 26 | 27 | - name: Install Node.js dependencies 28 | run: yarn 29 | 30 | - name: Run Prettier 31 | run: yarn fmt:check 32 | 33 | - name: Run ESLint/Flow 34 | run: yarn lint 35 | -------------------------------------------------------------------------------- /.github/workflows/packj.yml: -------------------------------------------------------------------------------- 1 | name: Packj Security Audit 2 | 3 | # Controls when the workflow will run 4 | on: 5 | pull_request: 6 | branches: 7 | - master 8 | paths: 9 | - "yarn.lock" 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | # This workflow contains a single job called "packj-audit" 14 | packj-security-audit: 15 | # The type of runner that the job will run on 16 | runs-on: ubuntu-latest 17 | 18 | # Steps represent a sequence of tasks that will be executed as part of the job 19 | steps: 20 | # Audit 21 | - name: Audit dependencies 22 | uses: ossillate-inc/packj-github-action@v0.0.7-beta 23 | 24 | with: 25 | DEPENDENCY_FILES: npm:package.json 26 | REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | semver: 7 | description: "The semver to use" 8 | required: true 9 | default: "patch" 10 | type: choice 11 | options: 12 | - auto 13 | - patch 14 | - minor 15 | - major 16 | - prerelease 17 | - prepatch 18 | - preminor 19 | - premajor 20 | baseTag: 21 | description: "base release tag" 22 | tag: 23 | description: "The npm tag" 24 | required: false 25 | default: "latest" 26 | commit-message: 27 | description: "The commit message template" 28 | required: false 29 | default: "Release {version}" 30 | pull_request: 31 | types: [closed] 32 | 33 | jobs: 34 | release: 35 | runs-on: ubuntu-latest 36 | permissions: 37 | contents: write 38 | issues: write 39 | pull-requests: write 40 | # optional: `id-token: write` permission is required if `provenance: true` is set below 41 | id-token: write 42 | steps: 43 | - name: Set up Node.js 44 | uses: actions/setup-node@v2 45 | with: 46 | node-version: 18 47 | 48 | - uses: nearform-actions/optic-release-automation-action@v4 49 | with: 50 | commit-message: ${{ github.event.inputs.commit-message }} 51 | semver: ${{ github.event.inputs.semver }} 52 | npm-tag: ${{ github.event.inputs.tag }} 53 | # optional: set this secret in your repo config for publishing to NPM 54 | npm-token: ${{ secrets.NPM_TOKEN }} 55 | # optional: NPM will generate provenance statement, or abort release if it can't 56 | provenance: true 57 | build-command: | 58 | yarn 59 | yarn build 60 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues and PRs" 2 | on: 3 | schedule: 4 | - cron: "0 0,6,12,18 * * *" 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v1 11 | permissions: 12 | issues: write 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this issue will be closed in 7 days" 16 | stale-pr-message: "This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this PR will be closed in 7 days" 17 | stale-pr-label: "stale" 18 | exempt-pr-label: "stale" 19 | days-before-stale: 90 20 | days-before-close: 30 21 | operations-per-run: 30 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | # Trigger the workflow on push or pull request, 4 | # but only for the master branch 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out Git repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: 18 22 | 23 | - name: Install Node.js dependencies 24 | run: yarn 25 | 26 | - name: Run tests 27 | run: yarn test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist/* 4 | !dist/react-grid-layout.min* 5 | !dist/*.html 6 | npm-debug.log 7 | build/ 8 | .idea 9 | coverage/ 10 | # Ignore built example files 11 | examples/*.html 12 | examples/*.js 13 | !examples/generate.js 14 | !examples/vars.js 15 | .vscode 16 | *.tgz 17 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | yarn flow 6 | make test -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | test/ 3 | examples/ 4 | yarn.lock 5 | .github 6 | *.config.js 7 | Makefile 8 | *.sh 9 | *.*.json 10 | __tests__/ 11 | __mocks__/ 12 | index-dev.js 13 | index-dev.html 14 | index.html 15 | flow-typed/ 16 | interfaces/ 17 | .husky 18 | .travis.yml 19 | *.png 20 | *.jpg 21 | .packj.yaml 22 | -------------------------------------------------------------------------------- /.packj.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Audit policies 3 | # 4 | audit: 5 | alerts: 6 | # 7 | # category: malicious packages (publicly known and unknown) 8 | # 9 | malicious: 10 | contains known malware: 11 | - reason: package is known to contain a dangerous malware 12 | - enabled: true 13 | typo-squatting or repo-jacking package: 14 | - reason: package impersonates another popular package to propagate malware 15 | - enabled: true 16 | 17 | # 18 | # alert category: suspicious packages (potentially malicious) 19 | # 20 | suspicious: 21 | inconsistent with repo source: 22 | - reason: package code inconsistent with the public repo source code 23 | - enabled: false # WIP 24 | overwrites system binaries: 25 | - reason: package code inconsistent with the public repo source code 26 | - enabled: false # WIP 27 | 28 | # 29 | # alert category: packages vulnerable to code exploits 30 | # 31 | vulnerable: 32 | contains known vulnerabilities: 33 | - reason: known vulnerabilities (CVEs) in package code could be exploited 34 | - enabled: true 35 | insecure network communication: 36 | - reason: package code uses insecure network communication (not https) 37 | - enabled: false # WIP 38 | 39 | # 40 | # packages with undesirable or "risky" attributes 41 | # 42 | undesirable: 43 | package is old or abandoned: 44 | - reason: old or abandoned packages receive no security updates and are risky 45 | - enabled: true 46 | 47 | invalid or no author email: 48 | - reason: a package with lack of or invalid author email suggests 2FA not enabled 49 | - enabled: true 50 | 51 | invalid or no homepage: 52 | - reason: a package with no or invalid homepage may not be preferable 53 | - enabled: false 54 | 55 | no source repo: 56 | - reason: lack of public source repo may suggest malicious intention 57 | - enabled: true 58 | 59 | fewer downloads: 60 | - reason: a package with few downloads may not be preferable 61 | - enabled: true 62 | 63 | no or insufficient readme: 64 | - reason: a package with lack of documentation may not be preferable 65 | - enabled: false 66 | 67 | fewer versions or releases: 68 | - reason: few versions suggest unstable or inactive project 69 | - enabled: true 70 | 71 | too many dependencies: 72 | - reason: too many dependencies increase attack surface 73 | - enabled: false 74 | 75 | version release after a long gap: 76 | - reason: a release after a long time may indicate account hijacking 77 | - enabled: false 78 | 79 | contains custom installation hooks: 80 | - reason: custom installation hooks may download or execute malicious code 81 | - enabled: false # WIP 82 | 83 | # 84 | # type: repo stats 85 | # 86 | few source repo stars: 87 | - reason: a package with few repo stars may not be preferable 88 | - enabled: false 89 | 90 | few source repo forks: 91 | - reason: a package with few repo forks may not be preferable 92 | - enabled: false 93 | 94 | forked source repo: 95 | - reason: a forked copy of a popular package may contain malicious code 96 | - enabled: true 97 | 98 | # 99 | # type: APIs and permissions 100 | # 101 | generates new code: 102 | - reason: package generates new code at runtime, which could be malicious 103 | - enabled: false 104 | forks or exits OS processes: 105 | - reason: package spawns new operating system processes, which could be malicious 106 | - enabled: false 107 | accesses obfuscated (hidden) code: 108 | - enabled: true 109 | accesses environment variables: 110 | - enabled: false 111 | changes system/environment variables: 112 | - enabled: false 113 | accesses files and dirs: 114 | - enabled: false 115 | communicates with external network: 116 | - enabled: false 117 | reads user input: 118 | - enabled: false 119 | 120 | # 121 | # Sandboxing policies 122 | # 123 | sandbox: 124 | rules: 125 | # 126 | # File system (allow or block accesses to file/dirs) 127 | # 128 | # ~/ represents home dir 129 | # . represents cwd dir 130 | # 131 | # NOTE: only ONE 'allow' and 'block' lines are allowed 132 | # 133 | fs: 134 | # TODO: customize as per your threat model 135 | 136 | # block access to home dir and all other locations (except the ones below) 137 | block: ~/, / 138 | allow: ., ~/.cache, ~/.npm, ~/.local, ~/.ruby, /tmp, /proc, /etc, /var, /bin, /usr/include, /usr/local, /usr/bin, /usr/lib, /usr/share, /lib 139 | 140 | # 141 | # Network (allow or block domains/ports) 142 | # 143 | # NOTE: only ONE 'allow' and 'block' lines are allowed 144 | # 145 | network: 146 | # TODO: customize as per your threat model 147 | 148 | # block all external network communication (except the ones below) 149 | block: 0.0.0.0 150 | 151 | # For NPM packages 152 | allow: registry.yarnpkg.com:0, npmjs.org:0, npmjs.com:0 153 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | examples/ 3 | __mocks__/ 4 | flow-typed/ 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "none", 10 | "bracketSpacing": true, 11 | "arrowParens": "avoid", 12 | "requirePragma": false, 13 | "insertPragma": false, 14 | "proseWrap": "preserve", 15 | "htmlWhitespaceSensitivity": "css", 16 | "endOfLine": "auto" 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 12 5 | - "lts/*" 6 | script: 7 | - make build test 8 | cache: yarn 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Samuel Reed 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DELETE_ON_ERROR: 2 | 3 | EXEC = npm exec -- 4 | DIST = ./dist 5 | BUILD = ./build 6 | LIB = ./lib 7 | TEST = ./test 8 | EXAMPLES = ./examples/*.{js,html} 9 | MIN = $(DIST)/react-grid-layout.min.js 10 | MIN_MAP = $(DIST)/react-grid-layout.min.js.map 11 | 12 | .PHONY: test dev lint build clean install link 13 | 14 | 15 | build: clean build-js $(MIN) 16 | 17 | clean: 18 | rm -rf $(BUILD) $(DIST) 19 | 20 | clean-example: 21 | rm -rf $(EXAMPLES) 22 | 23 | dev: 24 | @$(EXEC) webpack serve --config webpack-dev-server.config.js \ 25 | --hot --progress 26 | 27 | # Allows usage of `make install`, `make link` 28 | install link: 29 | @npm $@ 30 | 31 | # Build browser module 32 | dist/%.min.js: $(LIB) $(BIN) 33 | @$(EXEC) webpack 34 | 35 | build-js: 36 | @$(EXEC) babel --out-dir $(BUILD) $(LIB) 37 | 38 | # Will build for use on github pages. Full url of page is 39 | # https://react-grid-layout.github.io/react-grid-layout/examples/00-showcase.html 40 | # so the CONTENT_BASE should adapt. 41 | build-example: build clean-example 42 | @$(EXEC) webpack --config webpack-examples.config.js 43 | env CONTENT_BASE="/react-grid-layout/examples/" node ./examples/util/generate.js 44 | 45 | # Note: this doesn't hot reload, you need to re-run to update files. 46 | # TODO fix that 47 | view-example: build-example 48 | @$(EXEC) webpack serve --config webpack-examples.config.js --progress 49 | 50 | # FIXME flow is usually global 51 | lint: 52 | @$(EXEC) flow 53 | @$(EXEC) eslint --ext .js,.jsx 54 | 55 | test: 56 | env NODE_ENV=test $(EXEC) jest --coverage 57 | 58 | test-watch: 59 | env NODE_ENV=test $(EXEC) jest --watch 60 | 61 | test-update-snapshots: 62 | env NODE_ENV=test $(EXEC) jest --updateSnapshot 63 | 64 | release-patch: build lint test 65 | @$(call release,patch) 66 | 67 | release-minor: build lint test 68 | @$(call release,minor) 69 | 70 | release-major: build lint test 71 | @$(call release,major) 72 | 73 | publish: 74 | git push --tags origin HEAD:master 75 | npm publish 76 | 77 | define release 78 | VERSION=`node -pe "require('./package.json').version"` && \ 79 | NEXT_VERSION=`node -pe "require('semver').inc(\"$$VERSION\", '$(1)')"` && \ 80 | node -e "\ 81 | ['./package.json'].forEach(function(fileName) {\ 82 | var j = require(fileName);\ 83 | j.version = \"$$NEXT_VERSION\";\ 84 | var s = JSON.stringify(j, null, 2);\ 85 | require('fs').writeFileSync(fileName, s + '\\n');\ 86 | });" && \ 87 | git add package.json CHANGELOG.md $(MIN) $(MIN_MAP) && \ 88 | git commit -nm "release $$NEXT_VERSION" && \ 89 | git tag "$$NEXT_VERSION" -m "release $$NEXT_VERSION" 90 | npm pack --dry-run 91 | endef 92 | -------------------------------------------------------------------------------- /__mocks__/react-grid-layout.js: -------------------------------------------------------------------------------- 1 | // Used by jest as it requires some example files, which pull from 'react-grid-layout' 2 | module.exports = require('../index-dev'); -------------------------------------------------------------------------------- /css/styles.css: -------------------------------------------------------------------------------- 1 | .react-grid-layout { 2 | position: relative; 3 | transition: height 200ms ease; 4 | } 5 | .react-grid-item { 6 | transition: all 200ms ease; 7 | transition-property: left, top, width, height; 8 | } 9 | .react-grid-item img { 10 | pointer-events: none; 11 | user-select: none; 12 | } 13 | .react-grid-item.cssTransforms { 14 | transition-property: transform, width, height; 15 | } 16 | .react-grid-item.resizing { 17 | transition: none; 18 | z-index: 1; 19 | will-change: width, height; 20 | } 21 | 22 | .react-grid-item.react-draggable-dragging { 23 | transition: none; 24 | z-index: 3; 25 | will-change: transform; 26 | } 27 | 28 | .react-grid-item.dropping { 29 | visibility: hidden; 30 | } 31 | 32 | .react-grid-item.react-grid-placeholder { 33 | background: red; 34 | opacity: 0.2; 35 | transition-duration: 100ms; 36 | z-index: 2; 37 | -webkit-user-select: none; 38 | -moz-user-select: none; 39 | -ms-user-select: none; 40 | -o-user-select: none; 41 | user-select: none; 42 | } 43 | 44 | .react-grid-item.react-grid-placeholder.placeholder-resizing { 45 | transition: none; 46 | } 47 | 48 | .react-grid-item > .react-resizable-handle { 49 | position: absolute; 50 | width: 20px; 51 | height: 20px; 52 | } 53 | 54 | .react-grid-item > .react-resizable-handle::after { 55 | content: ""; 56 | position: absolute; 57 | right: 3px; 58 | bottom: 3px; 59 | width: 5px; 60 | height: 5px; 61 | border-right: 2px solid rgba(0, 0, 0, 0.4); 62 | border-bottom: 2px solid rgba(0, 0, 0, 0.4); 63 | } 64 | 65 | .react-resizable-hide > .react-resizable-handle { 66 | display: none; 67 | } 68 | 69 | .react-grid-item > .react-resizable-handle.react-resizable-handle-sw { 70 | bottom: 0; 71 | left: 0; 72 | cursor: sw-resize; 73 | transform: rotate(90deg); 74 | } 75 | .react-grid-item > .react-resizable-handle.react-resizable-handle-se { 76 | bottom: 0; 77 | right: 0; 78 | cursor: se-resize; 79 | } 80 | .react-grid-item > .react-resizable-handle.react-resizable-handle-nw { 81 | top: 0; 82 | left: 0; 83 | cursor: nw-resize; 84 | transform: rotate(180deg); 85 | } 86 | .react-grid-item > .react-resizable-handle.react-resizable-handle-ne { 87 | top: 0; 88 | right: 0; 89 | cursor: ne-resize; 90 | transform: rotate(270deg); 91 | } 92 | .react-grid-item > .react-resizable-handle.react-resizable-handle-w, 93 | .react-grid-item > .react-resizable-handle.react-resizable-handle-e { 94 | top: 50%; 95 | margin-top: -10px; 96 | cursor: ew-resize; 97 | } 98 | .react-grid-item > .react-resizable-handle.react-resizable-handle-w { 99 | left: 0; 100 | transform: rotate(135deg); 101 | } 102 | .react-grid-item > .react-resizable-handle.react-resizable-handle-e { 103 | right: 0; 104 | transform: rotate(315deg); 105 | } 106 | .react-grid-item > .react-resizable-handle.react-resizable-handle-n, 107 | .react-grid-item > .react-resizable-handle.react-resizable-handle-s { 108 | left: 50%; 109 | margin-left: -10px; 110 | cursor: ns-resize; 111 | } 112 | .react-grid-item > .react-resizable-handle.react-resizable-handle-n { 113 | top: 0; 114 | transform: rotate(225deg); 115 | } 116 | .react-grid-item > .react-resizable-handle.react-resizable-handle-s { 117 | bottom: 0; 118 | transform: rotate(45deg); 119 | } 120 | -------------------------------------------------------------------------------- /examples/util/example-styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 20px; 3 | } 4 | #content { 5 | width: 100%; 6 | } 7 | .react-grid-layout { 8 | background: #eee; 9 | margin-top: 10px; 10 | } 11 | .layoutJSON { 12 | background: #ddd; 13 | border: 1px solid black; 14 | margin-top: 10px; 15 | padding: 10px; 16 | } 17 | .columns { 18 | -moz-columns: 120px; 19 | -webkit-columns: 120px; 20 | columns: 120px; 21 | } 22 | .react-grid-item { 23 | box-sizing: border-box; 24 | } 25 | .react-grid-item:not(.react-grid-placeholder) { 26 | background: #ccc; 27 | border: 1px solid black; 28 | } 29 | .react-grid-item.resizing { 30 | opacity: 0.9; 31 | } 32 | .react-grid-item.static { 33 | background: #cce; 34 | } 35 | .react-grid-item .text { 36 | font-size: 24px; 37 | text-align: center; 38 | position: absolute; 39 | top: 0; 40 | bottom: 0; 41 | left: 0; 42 | right: 0; 43 | margin: auto; 44 | height: 24px; 45 | } 46 | .react-grid-item .minMax { 47 | font-size: 12px; 48 | } 49 | .react-grid-item .add { 50 | cursor: pointer; 51 | } 52 | .react-grid-dragHandleExample { 53 | cursor: move; /* fallback if grab cursor is unsupported */ 54 | cursor: grab; 55 | cursor: -moz-grab; 56 | cursor: -webkit-grab; 57 | } 58 | 59 | .toolbox { 60 | background-color: #dfd; 61 | width: 100%; 62 | height: 120px; 63 | overflow: scroll; 64 | } 65 | 66 | .hide-button { 67 | cursor: pointer; 68 | position: absolute; 69 | font-size: 20px; 70 | top: 0px; 71 | right: 5px; 72 | } 73 | 74 | .toolbox__title { 75 | font-size: 24px; 76 | margin-bottom: 5px; 77 | } 78 | .toolbox__items { 79 | display: block; 80 | } 81 | .toolbox__items__item { 82 | display: inline-block; 83 | text-align: center; 84 | line-height: 40px; 85 | cursor: pointer; 86 | width: 40px; 87 | height: 40px; 88 | padding: 10px; 89 | margin: 5px; 90 | border: 1px solid black; 91 | background-color: #ddd; 92 | } 93 | .droppable-element { 94 | width: 150px; 95 | text-align: center; 96 | background: #fdd; 97 | border: 1px solid black; 98 | margin: 10px 0; 99 | padding: 10px; 100 | } 101 | -------------------------------------------------------------------------------- /examples/util/generate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const fs = require("fs"); 3 | const ejs = require("ejs"); 4 | const path = require("path"); 5 | const data = require("./vars"); 6 | const tpl = fs.readFileSync(__dirname + "/template.ejs").toString(); 7 | const version = require('../../package.json').version; 8 | 9 | const base = process.env.CONTENT_BASE; 10 | if (typeof base !== "string") { 11 | throw new Error("env CONTENT_BASE is required to be set."); 12 | } 13 | const banner = 14 | "Do not edit this file! It is generated by `generate.js` in this folder, from `template.ejs` and " + 15 | "vars.js."; 16 | 17 | data.forEach(function(datum, i) { 18 | datum.base = base.replace(/\/$/, ""); // trim trailing slash 19 | datum.index = (i).toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping:false}); 20 | datum.banner = banner; 21 | datum.previous = data[i - 1]; 22 | datum.next = data[i + 1]; 23 | datum.version = version; 24 | }); 25 | 26 | data.forEach(function(datum, i) { 27 | const html = ejs.render(tpl, datum); 28 | fs.writeFileSync(path.resolve(__dirname, '..', `${datum.index}-${datum.source}.html`), html); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/util/template.ejs: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | RGL Example <%= index %> - <%= title %> 12 | 13 | 14 |

React-Grid-Layout 15 | v<%= version %> 16 | Demo <%= index %> - <%= title %> 17 |

18 | 30 | <%_ for(var i = 0; i < paragraphs.length; i++) { -%> 31 |

<%- paragraphs[i] %>

32 | <%_ } -%> 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/util/vars.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = [ 4 | { 5 | title: "Showcase", 6 | source: "showcase", 7 | paragraphs: [ 8 | "React-Grid-Layout is a grid layout system for React. It features auto-packing, draggable and resizable " + 9 | "widgets, static widgets, a fluid layout, and separate layouts per responsive breakpoint.", 10 | "Try it out! Drag some boxes around, resize them, and resize the window to see the responsive breakpoints." 11 | ] 12 | }, 13 | { 14 | title: "Basic", 15 | source: "basic", 16 | paragraphs: [ 17 | "Try dragging the elements around.", 18 | "This is a basic, non-responsive layout with dragging and resizing. Usage is very simple." 19 | ] 20 | }, 21 | { 22 | title: "No Dragging", 23 | source: "no-dragging", 24 | paragraphs: [ 25 | "This particular example has dragging and resizing turned off." 26 | ] 27 | }, 28 | { 29 | title: "Messy", 30 | source: "messy", 31 | paragraphs: [ 32 | "This demo shows what happens when elements are placed randomly all over the layout.", 33 | "RGL does not auto-pack in the same fashion as other projects, such as jQuery Masonry. Packing is only done " + 34 | "in the vertical dimension. If objects all have the same width, they will be packed efficiently.", 35 | "If a layout is fed to RGL that has items with incorrect dimensions (width too big, overlapping other elements, " + 36 | "out of bounds, etc), they will be automatically corrected on startup. See the " + 37 | "source of this demo, where elements are placed randomly in the layout." 38 | ] 39 | }, 40 | { 41 | title: "Grid Item Properties", 42 | source: "grid-property", 43 | paragraphs: [ 44 | "This demo uses a layout assigned on the grid items themselves as the data-grid property." 45 | ] 46 | }, 47 | { 48 | title: "Static Elements", 49 | source: "static-elements", 50 | paragraphs: [ 51 | "This demo sets an item to static. Static elements cannot be moved or resized. Other elements " + 52 | "move themselves around a static element." 53 | ] 54 | }, 55 | { 56 | title: "Dynamic Add/Remove", 57 | source: "dynamic-add-remove", 58 | paragraphs: [ 59 | "This demo shows what happens when items are dynamically added and removed.", 60 | 'You can remove an item by clicking its "x", and add a new one with the button.', 61 | "To further illustration RGL's capacities, this particular example is responsive. Trying resizing the window." 62 | ] 63 | }, 64 | { 65 | title: "LocalStorage", 66 | source: "localstorage", 67 | paragraphs: [ 68 | "This simple demo synchronizes to localStorage.", 69 | "Try moving and resizing elements, then reloading." 70 | ] 71 | }, 72 | { 73 | title: "Responsive with LocalStorage", 74 | source: "localstorage-responsive", 75 | paragraphs: [ 76 | "This simple demo synchronizes to localStorage for each responsive breakpoint.", 77 | "Try moving and resizing elements, changing window width, moving some more, and refreshing.", 78 | "Each breakpoint has a separate layout. The onLayoutChange callback calls back with " + 79 | "a hash of breakpoints to layouts, which is then synchronized to localStorage." 80 | ] 81 | }, 82 | { 83 | title: "Minimum and Maximum Width/Height", 84 | source: "min-max-wh", 85 | paragraphs: [ 86 | "You can set min and max dimensions on a grid item by using the `minW`, `maxW`, `minH`, and `maxH` properties.", 87 | "In this demo, the min and max dimensions are generated automatically. Try resizing the items below.", 88 | "If your mins and maxes collide: for example min > max, or the initial dimensions are out of range, " + 89 | "an error will be thrown." 90 | ] 91 | }, 92 | { 93 | title: "Dynamic Minimum and Maximum Width/Height", 94 | source: "dynamic-min-max-wh", 95 | paragraphs: [ 96 | "Your application may have more complex rules for determining an element's mins and maxes. This demo " + 97 | "demonstrates how to use the `onResize` handler to accomplish this.", 98 | "In this grid, all elements are allowed a max width of 2 if the height < 3, " + 99 | "and a min width of 2 if the height >= 3." 100 | ] 101 | }, 102 | { 103 | title: "No Vertical Compacting (Free Movement)", 104 | source: "no-vertical-compact", 105 | paragraphs: [ 106 | "You may want to turn off vertical compacting so items can be placed anywhere in the grid. Set the " + 107 | "property `compactType` to `null` to achieve this effect." 108 | ] 109 | }, 110 | { 111 | title: "Prevent Collision", 112 | source: "prevent-collision", 113 | paragraphs: [ 114 | "You may want to turn off rearrangement so items don't move arround when dragging. Set the " + 115 | "property `preventCollision` to `true` to achieve this effect. " + 116 | "It's particularly useful with `compactType` set to `null`." 117 | ] 118 | }, 119 | { 120 | title: "Error Case", 121 | source: "error-case", 122 | paragraphs: [ 123 | "This is an extra test case for a collision bug fixed in November 2017. When you drag 1 over 2, it should not " + 124 | "move over 3." 125 | ] 126 | }, 127 | { 128 | title: "Toolbox", 129 | source: "toolbox", 130 | paragraphs: [ 131 | "This demonstrates how to implement a toolbox to add and remove widgets. Click the 'X' on a widget to move it into the toolbox." 132 | ] 133 | }, 134 | { 135 | title: "Drag From Outside", 136 | source: "drag-from-outside", 137 | paragraphs: [ 138 | "This demo shows what happens when an item is added from outside of the grid.", 139 | "Once you drop the item within the grid you'll get its coordinates/properties and can perform actions with " + 140 | "it accordingly." 141 | ] 142 | }, 143 | { 144 | title: "Bounded", 145 | source: "bounded", 146 | paragraphs: [ 147 | "Try dragging the elements around. They can only be moved within the grid, the draggable placeholder will not show outside it." 148 | ] 149 | }, 150 | { 151 | title: "Bootstrap-style Responsive Grid", 152 | source: "responsive-bootstrap-style", 153 | paragraphs: [ 154 | "This demonstrates how to use ResponsiveGridLayout to create a Bootstrap-style responsive grid." 155 | ] 156 | }, 157 | { 158 | title: "Scale", 159 | source: "scale", 160 | paragraphs: [ 161 | "This demonstrates how to compensate for a scaled parent." 162 | ] 163 | }, 164 | { 165 | title: "Allow Overlap", 166 | source: "allow-overlap", 167 | paragraphs: [ 168 | "This demonstrates how to overlap grid items." 169 | ] 170 | }, 171 | { 172 | title: "All Resizable Handles", 173 | source: "resizable-handles", 174 | paragraphs: [ 175 | "This shows a grid with all resizable handles enabled. See the prop `resizableHandles` on the grid and grid items in the README." 176 | ] 177 | }, 178 | { 179 | title: "Single Row Horizontal", 180 | source: "horizontal", 181 | paragraphs: [ 182 | "This demonstrates how to constrain the elements to a single row." 183 | ] 184 | }, 185 | ]; 186 | -------------------------------------------------------------------------------- /flow-typed/npm/clsx_v1.x.x.js: -------------------------------------------------------------------------------- 1 | declare module 'clsx' { 2 | declare type Classes = 3 | | Array 4 | | { [className: string]: *, ... } 5 | | string 6 | | number 7 | | boolean 8 | | void 9 | | null; 10 | 11 | declare module.exports: (...classes: Array) => string; 12 | } 13 | -------------------------------------------------------------------------------- /flow-typed/npm/enzyme_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 931d5482afcb022bcbddb841fd08d683 2 | // flow-typed version: 5175c53189/enzyme_v3.x.x/flow_>=v0.104.x 3 | 4 | declare module "enzyme" { 5 | declare type PredicateFunction> = ( 6 | wrapper: T, 7 | index: number 8 | ) => boolean; 9 | declare type UntypedSelector = string | { [key: string]: number|string|boolean, ... }; 10 | declare type EnzymeSelector = UntypedSelector | React$ElementType; 11 | 12 | // CheerioWrapper is a type alias for an actual cheerio instance 13 | // TODO: Reference correct type from cheerio's type declarations 14 | declare type CheerioWrapper = any; 15 | 16 | declare class Wrapper { 17 | equals(node: React$Element): boolean, 18 | find(selector: UntypedSelector): this, 19 | find(selector: T): ReactWrapper, 20 | findWhere(predicate: PredicateFunction): this, 21 | filter(selector: UntypedSelector): this, 22 | filter(selector: T): ReactWrapper, 23 | filterWhere(predicate: PredicateFunction): this, 24 | hostNodes(): this, 25 | contains(nodes: React$Node): boolean, 26 | containsMatchingElement(node: React$Node): boolean, 27 | containsAllMatchingElements(nodes: React$Node): boolean, 28 | containsAnyMatchingElements(nodes: React$Node): boolean, 29 | dive(option?: { context?: Object, ... }): this, 30 | exists(selector?: EnzymeSelector): boolean, 31 | isEmptyRender(): boolean, 32 | matchesElement(node: React$Node): boolean, 33 | hasClass(className: string): boolean, 34 | is(selector: EnzymeSelector): boolean, 35 | isEmpty(): boolean, 36 | not(selector: EnzymeSelector): this, 37 | children(selector?: UntypedSelector): this, 38 | children(selector: T): ReactWrapper, 39 | childAt(index: number): this, 40 | parents(selector?: UntypedSelector): this, 41 | parents(selector: T): ReactWrapper, 42 | parent(): this, 43 | closest(selector: UntypedSelector): this, 44 | closest(selector: T): ReactWrapper, 45 | render(): CheerioWrapper, 46 | renderProp(propName: string): (...args: Array) => this, 47 | unmount(): this, 48 | text(): string, 49 | html(): string, 50 | invoke(propName: string): (...args: $ReadOnlyArray) => mixed, 51 | get(index: number): React$Node, 52 | getDOMNode(): HTMLElement | HTMLInputElement, 53 | at(index: number): this, 54 | first(): this, 55 | last(): this, 56 | state(key?: string): any, 57 | context(key?: string): any, 58 | props(): Object, 59 | prop(key: string): any, 60 | key(): string, 61 | simulate(event: string, ...args: Array): this, 62 | simulateError(error: Error): this, 63 | slice(begin?: number, end?: number): this, 64 | setState(state: {...}, callback?: () => void): this, 65 | setProps(props: {...}, callback?: () => void): this, 66 | setContext(context: Object): this, 67 | instance(): React$ElementRef, 68 | update(): this, 69 | debug(options?: Object): string, 70 | type(): string | Function | null, 71 | name(): string, 72 | forEach(fn: (node: this, index: number) => mixed): this, 73 | map(fn: (node: this, index: number) => T): Array, 74 | reduce( 75 | fn: (value: T, node: this, index: number) => T, 76 | initialValue?: T 77 | ): Array, 78 | reduceRight( 79 | fn: (value: T, node: this, index: number) => T, 80 | initialValue?: T 81 | ): Array, 82 | some(selector: EnzymeSelector): boolean, 83 | someWhere(predicate: PredicateFunction): boolean, 84 | every(selector: EnzymeSelector): boolean, 85 | everyWhere(predicate: PredicateFunction): boolean, 86 | length: number 87 | } 88 | 89 | declare class ReactWrapper extends Wrapper { 90 | constructor(nodes: React$Element, root: any, options?: ?Object): ReactWrapper, 91 | mount(): this, 92 | ref(refName: string): this, 93 | detach(): void 94 | } 95 | 96 | declare class ShallowWrapper extends Wrapper { 97 | constructor( 98 | nodes: React$Element, 99 | root: any, 100 | options?: ?Object 101 | ): ShallowWrapper, 102 | equals(node: React$Node): boolean, 103 | shallow(options?: { context?: Object, ... }): ShallowWrapper, 104 | getElement(): React$Node, 105 | getElements(): Array 106 | } 107 | 108 | declare function shallow( 109 | node: React$Element, 110 | options?: { 111 | context?: Object, 112 | disableLifecycleMethods?: boolean, 113 | ... 114 | } 115 | ): ShallowWrapper; 116 | declare function mount( 117 | node: React$Element, 118 | options?: { 119 | context?: Object, 120 | attachTo?: HTMLElement, 121 | childContextTypes?: Object, 122 | ... 123 | } 124 | ): ReactWrapper; 125 | declare function render( 126 | node: React$Node, 127 | options?: { context?: Object, ... } 128 | ): CheerioWrapper; 129 | 130 | declare module.exports: { 131 | configure(options: { 132 | Adapter?: any, 133 | disableLifecycleMethods?: boolean, 134 | ... 135 | }): void, 136 | render: typeof render, 137 | mount: typeof mount, 138 | shallow: typeof shallow, 139 | ShallowWrapper: typeof ShallowWrapper, 140 | ReactWrapper: typeof ReactWrapper, 141 | ... 142 | }; 143 | } 144 | -------------------------------------------------------------------------------- /flow-typed/npm/prop-types_v15.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: c93a723cbeb4d2f95d6a472157f6052f 2 | // flow-typed version: 61b795e5b6/prop-types_v15.x.x/flow_>=v0.104.x 3 | 4 | type $npm$propTypes$ReactPropsCheckType = ( 5 | props: any, 6 | propName: string, 7 | componentName: string, 8 | href?: string 9 | ) => ?Error; 10 | 11 | // Copied from: https://github.com/facebook/flow/blob/0938da8d7293d0077fbe95c3a3e0eebadb57b012/lib/react.js#L433-L449 12 | declare module "prop-types" { 13 | declare var array: React$PropType$Primitive>; 14 | declare var bool: React$PropType$Primitive; 15 | declare var func: React$PropType$Primitive<(...a: Array) => mixed>; 16 | declare var number: React$PropType$Primitive; 17 | declare var object: React$PropType$Primitive<{ +[string]: mixed, ... }>; 18 | declare var string: React$PropType$Primitive; 19 | declare var symbol: React$PropType$Primitive; 20 | declare var any: React$PropType$Primitive; 21 | declare var arrayOf: React$PropType$ArrayOf; 22 | declare var element: React$PropType$Primitive; 23 | declare var elementType: React$PropType$Primitive; 24 | declare var instanceOf: React$PropType$InstanceOf; 25 | declare var node: React$PropType$Primitive; 26 | declare var objectOf: React$PropType$ObjectOf; 27 | declare var oneOf: React$PropType$OneOf; 28 | declare var oneOfType: React$PropType$OneOfType; 29 | declare var shape: React$PropType$Shape; 30 | 31 | declare function checkPropTypes( 32 | propTypes: { [key: $Keys]: $npm$propTypes$ReactPropsCheckType, ... }, 33 | values: V, 34 | location: string, 35 | componentName: string, 36 | getStack: ?() => ?string 37 | ): void; 38 | } 39 | -------------------------------------------------------------------------------- /index-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RGL Example 5 | 6 | 7 |

React-Grid-Layout Dev

8 |

Try dragging the elements around.

9 |

10 | If you don't see any content, run webpack (npm run dev) and load this file from the server it opens. 14 |

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /index-dev.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/ReactGridLayout").default; 2 | module.exports.utils = require("./lib/utils"); 3 | module.exports.calculateUtils = require("./lib/calculateUtils"); 4 | module.exports.Responsive = require("./lib/ResponsiveReactGridLayout").default; 5 | module.exports.Responsive.utils = require("./lib/responsiveUtils"); 6 | module.exports.WidthProvider = 7 | require("./lib/components/WidthProvider").default; 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./build/ReactGridLayout").default; 2 | module.exports.utils = require("./build/utils"); 3 | module.exports.calculateUtils = require("./build/calculateUtils"); 4 | module.exports.Responsive = 5 | require("./build/ResponsiveReactGridLayout").default; 6 | module.exports.Responsive.utils = require("./build/responsiveUtils"); 7 | module.exports.WidthProvider = 8 | require("./build/components/WidthProvider").default; 9 | -------------------------------------------------------------------------------- /index.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as utils from "./lib/utils"; 4 | export { default } from "./lib/ReactGridLayout"; 5 | export { default as Responsive } from "./lib/ResponsiveReactGridLayout"; 6 | export { default as WidthProvider } from "./lib/components/WidthProvider"; 7 | 8 | export { utils }; 9 | -------------------------------------------------------------------------------- /lib/GridItem.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | import { DraggableCore } from "react-draggable"; 5 | import { Resizable } from "react-resizable"; 6 | import { 7 | fastPositionEqual, 8 | perc, 9 | resizeItemInDirection, 10 | setTopLeft, 11 | setTransform 12 | } from "./utils"; 13 | import { 14 | calcGridItemPosition, 15 | calcGridItemWHPx, 16 | calcGridColWidth, 17 | calcXY, 18 | calcWH, 19 | clamp 20 | } from "./calculateUtils"; 21 | import { 22 | resizeHandleAxesType, 23 | resizeHandleType 24 | } from "./ReactGridLayoutPropTypes"; 25 | import clsx from "clsx"; 26 | import type { Element as ReactElement, Node as ReactNode } from "react"; 27 | 28 | import type { 29 | ReactDraggableCallbackData, 30 | GridDragEvent, 31 | GridResizeEvent, 32 | DroppingPosition, 33 | Position, 34 | ResizeHandleAxis 35 | } from "./utils"; 36 | 37 | import type { PositionParams } from "./calculateUtils"; 38 | import type { ResizeHandle, ReactRef } from "./ReactGridLayoutPropTypes"; 39 | 40 | type PartialPosition = { top: number, left: number }; 41 | type GridItemCallback = ( 42 | i: string, 43 | w: number, 44 | h: number, 45 | Data 46 | ) => void; 47 | 48 | type ResizeCallbackData = { 49 | node: HTMLElement, 50 | size: Position, 51 | handle: ResizeHandleAxis 52 | }; 53 | 54 | type GridItemResizeCallback = ( 55 | e: Event, 56 | data: ResizeCallbackData, 57 | position: Position 58 | ) => void; 59 | 60 | type State = { 61 | resizing: ?{ top: number, left: number, width: number, height: number }, 62 | dragging: ?{ top: number, left: number }, 63 | className: string 64 | }; 65 | 66 | type Props = { 67 | children: ReactElement, 68 | cols: number, 69 | containerWidth: number, 70 | margin: [number, number], 71 | containerPadding: [number, number], 72 | rowHeight: number, 73 | maxRows: number, 74 | isDraggable: boolean, 75 | isResizable: boolean, 76 | isBounded: boolean, 77 | static?: boolean, 78 | useCSSTransforms?: boolean, 79 | usePercentages?: boolean, 80 | transformScale: number, 81 | droppingPosition?: DroppingPosition, 82 | 83 | className: string, 84 | style?: Object, 85 | // Draggability 86 | cancel: string, 87 | handle: string, 88 | 89 | x: number, 90 | y: number, 91 | w: number, 92 | h: number, 93 | 94 | minW: number, 95 | maxW: number, 96 | minH: number, 97 | maxH: number, 98 | i: string, 99 | 100 | resizeHandles?: ResizeHandleAxis[], 101 | resizeHandle?: ResizeHandle, 102 | 103 | onDrag?: GridItemCallback, 104 | onDragStart?: GridItemCallback, 105 | onDragStop?: GridItemCallback, 106 | onResize?: GridItemCallback, 107 | onResizeStart?: GridItemCallback, 108 | onResizeStop?: GridItemCallback 109 | }; 110 | 111 | type DefaultProps = { 112 | className: string, 113 | cancel: string, 114 | handle: string, 115 | minH: number, 116 | minW: number, 117 | maxH: number, 118 | maxW: number, 119 | transformScale: number 120 | }; 121 | 122 | /** 123 | * An individual item within a ReactGridLayout. 124 | */ 125 | export default class GridItem extends React.Component { 126 | static propTypes = { 127 | // Children must be only a single element 128 | children: PropTypes.element, 129 | 130 | // General grid attributes 131 | cols: PropTypes.number.isRequired, 132 | containerWidth: PropTypes.number.isRequired, 133 | rowHeight: PropTypes.number.isRequired, 134 | margin: PropTypes.array.isRequired, 135 | maxRows: PropTypes.number.isRequired, 136 | containerPadding: PropTypes.array.isRequired, 137 | 138 | // These are all in grid units 139 | x: PropTypes.number.isRequired, 140 | y: PropTypes.number.isRequired, 141 | w: PropTypes.number.isRequired, 142 | h: PropTypes.number.isRequired, 143 | 144 | // All optional 145 | minW: function (props: Props, propName: string) { 146 | const value = props[propName]; 147 | if (typeof value !== "number") return new Error("minWidth not Number"); 148 | if (value > props.w || value > props.maxW) 149 | return new Error("minWidth larger than item width/maxWidth"); 150 | }, 151 | 152 | maxW: function (props: Props, propName: string) { 153 | const value = props[propName]; 154 | if (typeof value !== "number") return new Error("maxWidth not Number"); 155 | if (value < props.w || value < props.minW) 156 | return new Error("maxWidth smaller than item width/minWidth"); 157 | }, 158 | 159 | minH: function (props: Props, propName: string) { 160 | const value = props[propName]; 161 | if (typeof value !== "number") return new Error("minHeight not Number"); 162 | if (value > props.h || value > props.maxH) 163 | return new Error("minHeight larger than item height/maxHeight"); 164 | }, 165 | 166 | maxH: function (props: Props, propName: string) { 167 | const value = props[propName]; 168 | if (typeof value !== "number") return new Error("maxHeight not Number"); 169 | if (value < props.h || value < props.minH) 170 | return new Error("maxHeight smaller than item height/minHeight"); 171 | }, 172 | 173 | // ID is nice to have for callbacks 174 | i: PropTypes.string.isRequired, 175 | 176 | // Resize handle options 177 | resizeHandles: resizeHandleAxesType, 178 | resizeHandle: resizeHandleType, 179 | 180 | // Functions 181 | onDragStop: PropTypes.func, 182 | onDragStart: PropTypes.func, 183 | onDrag: PropTypes.func, 184 | onResizeStop: PropTypes.func, 185 | onResizeStart: PropTypes.func, 186 | onResize: PropTypes.func, 187 | 188 | // Flags 189 | isDraggable: PropTypes.bool.isRequired, 190 | isResizable: PropTypes.bool.isRequired, 191 | isBounded: PropTypes.bool.isRequired, 192 | static: PropTypes.bool, 193 | 194 | // Use CSS transforms instead of top/left 195 | useCSSTransforms: PropTypes.bool.isRequired, 196 | transformScale: PropTypes.number, 197 | 198 | // Others 199 | className: PropTypes.string, 200 | // Selector for draggable handle 201 | handle: PropTypes.string, 202 | // Selector for draggable cancel (see react-draggable) 203 | cancel: PropTypes.string, 204 | // Current position of a dropping element 205 | droppingPosition: PropTypes.shape({ 206 | e: PropTypes.object.isRequired, 207 | left: PropTypes.number.isRequired, 208 | top: PropTypes.number.isRequired 209 | }) 210 | }; 211 | 212 | static defaultProps: DefaultProps = { 213 | className: "", 214 | cancel: "", 215 | handle: "", 216 | minH: 1, 217 | minW: 1, 218 | maxH: Infinity, 219 | maxW: Infinity, 220 | transformScale: 1 221 | }; 222 | 223 | state: State = { 224 | resizing: null, 225 | dragging: null, 226 | className: "" 227 | }; 228 | 229 | elementRef: ReactRef = React.createRef(); 230 | 231 | shouldComponentUpdate(nextProps: Props, nextState: State): boolean { 232 | // We can't deeply compare children. If the developer memoizes them, we can 233 | // use this optimization. 234 | if (this.props.children !== nextProps.children) return true; 235 | if (this.props.droppingPosition !== nextProps.droppingPosition) return true; 236 | // TODO memoize these calculations so they don't take so long? 237 | const oldPosition = calcGridItemPosition( 238 | this.getPositionParams(this.props), 239 | this.props.x, 240 | this.props.y, 241 | this.props.w, 242 | this.props.h, 243 | this.state 244 | ); 245 | const newPosition = calcGridItemPosition( 246 | this.getPositionParams(nextProps), 247 | nextProps.x, 248 | nextProps.y, 249 | nextProps.w, 250 | nextProps.h, 251 | nextState 252 | ); 253 | return ( 254 | !fastPositionEqual(oldPosition, newPosition) || 255 | this.props.useCSSTransforms !== nextProps.useCSSTransforms 256 | ); 257 | } 258 | 259 | componentDidMount() { 260 | this.moveDroppingItem({}); 261 | } 262 | 263 | componentDidUpdate(prevProps: Props) { 264 | this.moveDroppingItem(prevProps); 265 | } 266 | 267 | // When a droppingPosition is present, this means we should fire a move event, as if we had moved 268 | // this element by `x, y` pixels. 269 | moveDroppingItem(prevProps: Props) { 270 | const { droppingPosition } = this.props; 271 | if (!droppingPosition) return; 272 | const node = this.elementRef.current; 273 | // Can't find DOM node (are we unmounted?) 274 | if (!node) return; 275 | 276 | const prevDroppingPosition = prevProps.droppingPosition || { 277 | left: 0, 278 | top: 0 279 | }; 280 | const { dragging } = this.state; 281 | 282 | const shouldDrag = 283 | (dragging && droppingPosition.left !== prevDroppingPosition.left) || 284 | droppingPosition.top !== prevDroppingPosition.top; 285 | 286 | if (!dragging) { 287 | this.onDragStart(droppingPosition.e, { 288 | node, 289 | deltaX: droppingPosition.left, 290 | deltaY: droppingPosition.top 291 | }); 292 | } else if (shouldDrag) { 293 | const deltaX = droppingPosition.left - dragging.left; 294 | const deltaY = droppingPosition.top - dragging.top; 295 | 296 | this.onDrag(droppingPosition.e, { 297 | node, 298 | deltaX, 299 | deltaY 300 | }); 301 | } 302 | } 303 | 304 | getPositionParams(props: Props = this.props): PositionParams { 305 | return { 306 | cols: props.cols, 307 | containerPadding: props.containerPadding, 308 | containerWidth: props.containerWidth, 309 | margin: props.margin, 310 | maxRows: props.maxRows, 311 | rowHeight: props.rowHeight 312 | }; 313 | } 314 | 315 | /** 316 | * This is where we set the grid item's absolute placement. It gets a little tricky because we want to do it 317 | * well when server rendering, and the only way to do that properly is to use percentage width/left because 318 | * we don't know exactly what the browser viewport is. 319 | * Unfortunately, CSS Transforms, which are great for performance, break in this instance because a percentage 320 | * left is relative to the item itself, not its container! So we cannot use them on the server rendering pass. 321 | * 322 | * @param {Object} pos Position object with width, height, left, top. 323 | * @return {Object} Style object. 324 | */ 325 | createStyle(pos: Position): { [key: string]: ?string } { 326 | const { usePercentages, containerWidth, useCSSTransforms } = this.props; 327 | 328 | let style; 329 | // CSS Transforms support (default) 330 | if (useCSSTransforms) { 331 | style = setTransform(pos); 332 | } else { 333 | // top,left (slow) 334 | style = setTopLeft(pos); 335 | 336 | // This is used for server rendering. 337 | if (usePercentages) { 338 | style.left = perc(pos.left / containerWidth); 339 | style.width = perc(pos.width / containerWidth); 340 | } 341 | } 342 | 343 | return style; 344 | } 345 | 346 | /** 347 | * Mix a Draggable instance into a child. 348 | * @param {Element} child Child element. 349 | * @return {Element} Child wrapped in Draggable. 350 | */ 351 | mixinDraggable( 352 | child: ReactElement, 353 | isDraggable: boolean 354 | ): ReactElement { 355 | return ( 356 | 369 | {child} 370 | 371 | ); 372 | } 373 | 374 | /** 375 | * Utility function to setup callback handler definitions for 376 | * similarily structured resize events. 377 | */ 378 | curryResizeHandler(position: Position, handler: Function): Function { 379 | return (e: Event, data: ResizeCallbackData): Function => 380 | handler(e, data, position); 381 | } 382 | 383 | /** 384 | * Mix a Resizable instance into a child. 385 | * @param {Element} child Child element. 386 | * @param {Object} position Position object (pixel values) 387 | * @return {Element} Child wrapped in Resizable. 388 | */ 389 | mixinResizable( 390 | child: ReactElement, 391 | position: Position, 392 | isResizable: boolean 393 | ): ReactElement { 394 | const { 395 | cols, 396 | minW, 397 | minH, 398 | maxW, 399 | maxH, 400 | transformScale, 401 | resizeHandles, 402 | resizeHandle 403 | } = this.props; 404 | const positionParams = this.getPositionParams(); 405 | 406 | // This is the max possible width - doesn't go to infinity because of the width of the window 407 | const maxWidth = calcGridItemPosition(positionParams, 0, 0, cols, 0).width; 408 | 409 | // Calculate min/max constraints using our min & maxes 410 | const mins = calcGridItemPosition(positionParams, 0, 0, minW, minH); 411 | const maxes = calcGridItemPosition(positionParams, 0, 0, maxW, maxH); 412 | const minConstraints = [mins.width, mins.height]; 413 | const maxConstraints = [ 414 | Math.min(maxes.width, maxWidth), 415 | Math.min(maxes.height, Infinity) 416 | ]; 417 | return ( 418 | 435 | {child} 436 | 437 | ); 438 | } 439 | 440 | /** 441 | * onDragStart event handler 442 | * @param {Event} e event data 443 | * @param {Object} callbackData an object with node, delta and position information 444 | */ 445 | onDragStart: (Event, ReactDraggableCallbackData) => void = (e, { node }) => { 446 | const { onDragStart, transformScale } = this.props; 447 | if (!onDragStart) return; 448 | 449 | const newPosition: PartialPosition = { top: 0, left: 0 }; 450 | 451 | // TODO: this wont work on nested parents 452 | const { offsetParent } = node; 453 | if (!offsetParent) return; 454 | const parentRect = offsetParent.getBoundingClientRect(); 455 | const clientRect = node.getBoundingClientRect(); 456 | const cLeft = clientRect.left / transformScale; 457 | const pLeft = parentRect.left / transformScale; 458 | const cTop = clientRect.top / transformScale; 459 | const pTop = parentRect.top / transformScale; 460 | newPosition.left = cLeft - pLeft + offsetParent.scrollLeft; 461 | newPosition.top = cTop - pTop + offsetParent.scrollTop; 462 | this.setState({ dragging: newPosition }); 463 | 464 | // Call callback with this data 465 | const { x, y } = calcXY( 466 | this.getPositionParams(), 467 | newPosition.top, 468 | newPosition.left, 469 | this.props.w, 470 | this.props.h 471 | ); 472 | 473 | return onDragStart.call(this, this.props.i, x, y, { 474 | e, 475 | node, 476 | newPosition 477 | }); 478 | }; 479 | 480 | /** 481 | * onDrag event handler 482 | * @param {Event} e event data 483 | * @param {Object} callbackData an object with node, delta and position information 484 | */ 485 | onDrag: (Event, ReactDraggableCallbackData) => void = ( 486 | e, 487 | { node, deltaX, deltaY } 488 | ) => { 489 | const { onDrag } = this.props; 490 | if (!onDrag) return; 491 | 492 | if (!this.state.dragging) { 493 | throw new Error("onDrag called before onDragStart."); 494 | } 495 | let top = this.state.dragging.top + deltaY; 496 | let left = this.state.dragging.left + deltaX; 497 | 498 | const { isBounded, i, w, h, containerWidth } = this.props; 499 | const positionParams = this.getPositionParams(); 500 | 501 | // Boundary calculations; keeps items within the grid 502 | if (isBounded) { 503 | const { offsetParent } = node; 504 | 505 | if (offsetParent) { 506 | const { margin, rowHeight, containerPadding } = this.props; 507 | const bottomBoundary = 508 | offsetParent.clientHeight - calcGridItemWHPx(h, rowHeight, margin[1]); 509 | top = clamp(top - containerPadding[1], 0, bottomBoundary); 510 | 511 | const colWidth = calcGridColWidth(positionParams); 512 | const rightBoundary = 513 | containerWidth - calcGridItemWHPx(w, colWidth, margin[0]); 514 | left = clamp(left - containerPadding[0], 0, rightBoundary); 515 | } 516 | } 517 | 518 | const newPosition: PartialPosition = { top, left }; 519 | this.setState({ dragging: newPosition }); 520 | 521 | // Call callback with this data 522 | const { x, y } = calcXY(positionParams, top, left, w, h); 523 | return onDrag.call(this, i, x, y, { 524 | e, 525 | node, 526 | newPosition 527 | }); 528 | }; 529 | 530 | /** 531 | * onDragStop event handler 532 | * @param {Event} e event data 533 | * @param {Object} callbackData an object with node, delta and position information 534 | */ 535 | onDragStop: (Event, ReactDraggableCallbackData) => void = (e, { node }) => { 536 | const { onDragStop } = this.props; 537 | if (!onDragStop) return; 538 | 539 | if (!this.state.dragging) { 540 | throw new Error("onDragEnd called before onDragStart."); 541 | } 542 | const { w, h, i } = this.props; 543 | const { left, top } = this.state.dragging; 544 | const newPosition: PartialPosition = { top, left }; 545 | this.setState({ dragging: null }); 546 | 547 | const { x, y } = calcXY(this.getPositionParams(), top, left, w, h); 548 | 549 | return onDragStop.call(this, i, x, y, { 550 | e, 551 | node, 552 | newPosition 553 | }); 554 | }; 555 | 556 | /** 557 | * onResizeStop event handler 558 | * @param {Event} e event data 559 | * @param {Object} callbackData an object with node and size information 560 | */ 561 | onResizeStop: GridItemResizeCallback = (e, callbackData, position) => 562 | this.onResizeHandler(e, callbackData, position, "onResizeStop"); 563 | 564 | // onResizeStart event handler 565 | onResizeStart: GridItemResizeCallback = (e, callbackData, position) => 566 | this.onResizeHandler(e, callbackData, position, "onResizeStart"); 567 | 568 | // onResize event handler 569 | onResize: GridItemResizeCallback = (e, callbackData, position) => 570 | this.onResizeHandler(e, callbackData, position, "onResize"); 571 | 572 | /** 573 | * Wrapper around resize events to provide more useful data. 574 | */ 575 | onResizeHandler( 576 | e: Event, 577 | { node, size, handle }: ResizeCallbackData, // 'size' is updated position 578 | position: Position, // existing position 579 | handlerName: string 580 | ): void { 581 | const handler = this.props[handlerName]; 582 | if (!handler) return; 583 | const { x, y, i, maxH, minH, containerWidth } = this.props; 584 | const { minW, maxW } = this.props; 585 | 586 | // Clamping of dimensions based on resize direction 587 | let updatedSize = size; 588 | if (node) { 589 | updatedSize = resizeItemInDirection( 590 | handle, 591 | position, 592 | size, 593 | containerWidth 594 | ); 595 | this.setState({ 596 | resizing: handlerName === "onResizeStop" ? null : updatedSize 597 | }); 598 | } 599 | 600 | // Get new XY based on pixel size 601 | let { w, h } = calcWH( 602 | this.getPositionParams(), 603 | updatedSize.width, 604 | updatedSize.height, 605 | x, 606 | y, 607 | handle 608 | ); 609 | 610 | // Min/max capping. 611 | // minW should be at least 1 (TODO propTypes validation?) 612 | w = clamp(w, Math.max(minW, 1), maxW); 613 | h = clamp(h, minH, maxH); 614 | 615 | handler.call(this, i, w, h, { e, node, size: updatedSize, handle }); 616 | } 617 | 618 | render(): ReactNode { 619 | const { 620 | x, 621 | y, 622 | w, 623 | h, 624 | isDraggable, 625 | isResizable, 626 | droppingPosition, 627 | useCSSTransforms 628 | } = this.props; 629 | 630 | const pos = calcGridItemPosition( 631 | this.getPositionParams(), 632 | x, 633 | y, 634 | w, 635 | h, 636 | this.state 637 | ); 638 | const child = React.Children.only(this.props.children); 639 | 640 | // Create the child element. We clone the existing element but modify its className and style. 641 | let newChild = React.cloneElement(child, { 642 | ref: this.elementRef, 643 | className: clsx( 644 | "react-grid-item", 645 | child.props.className, 646 | this.props.className, 647 | { 648 | static: this.props.static, 649 | resizing: Boolean(this.state.resizing), 650 | "react-draggable": isDraggable, 651 | "react-draggable-dragging": Boolean(this.state.dragging), 652 | dropping: Boolean(droppingPosition), 653 | cssTransforms: useCSSTransforms 654 | } 655 | ), 656 | // We can set the width and height on the child, but unfortunately we can't set the position. 657 | style: { 658 | ...this.props.style, 659 | ...child.props.style, 660 | ...this.createStyle(pos) 661 | } 662 | }); 663 | 664 | // Resizable support. This is usually on but the user can toggle it off. 665 | newChild = this.mixinResizable(newChild, pos, isResizable); 666 | 667 | // Draggable support. This is always on, except for with placeholders. 668 | newChild = this.mixinDraggable(newChild, isDraggable); 669 | 670 | return newChild; 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /lib/ReactGridLayout.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from "react"; 3 | 4 | import { deepEqual } from "fast-equals"; 5 | import clsx from "clsx"; 6 | import { 7 | bottom, 8 | childrenEqual, 9 | cloneLayoutItem, 10 | compact, 11 | compactType, 12 | fastRGLPropsEqual, 13 | getAllCollisions, 14 | getLayoutItem, 15 | moveElement, 16 | noop, 17 | synchronizeLayoutWithChildren, 18 | withLayoutItem 19 | } from "./utils"; 20 | 21 | import { calcXY } from "./calculateUtils"; 22 | 23 | import GridItem from "./GridItem"; 24 | import ReactGridLayoutPropTypes from "./ReactGridLayoutPropTypes"; 25 | import type { 26 | ChildrenArray as ReactChildrenArray, 27 | Element as ReactElement 28 | } from "react"; 29 | 30 | // Types 31 | import type { 32 | CompactType, 33 | GridResizeEvent, 34 | GridDragEvent, 35 | DragOverEvent, 36 | Layout, 37 | DroppingPosition, 38 | LayoutItem 39 | } from "./utils"; 40 | 41 | import type { PositionParams } from "./calculateUtils"; 42 | 43 | type State = { 44 | activeDrag: ?LayoutItem, 45 | layout: Layout, 46 | mounted: boolean, 47 | oldDragItem: ?LayoutItem, 48 | oldLayout: ?Layout, 49 | oldResizeItem: ?LayoutItem, 50 | resizing: boolean, 51 | droppingDOMNode: ?ReactElement, 52 | droppingPosition?: DroppingPosition, 53 | // Mirrored props 54 | children: ReactChildrenArray>, 55 | compactType?: CompactType, 56 | propsLayout?: Layout 57 | }; 58 | 59 | import type { Props, DefaultProps } from "./ReactGridLayoutPropTypes"; 60 | 61 | // End Types 62 | 63 | const layoutClassName = "react-grid-layout"; 64 | let isFirefox = false; 65 | // Try...catch will protect from navigator not existing (e.g. node) or a bad implementation of navigator 66 | try { 67 | isFirefox = /firefox/i.test(navigator.userAgent); 68 | } catch (e) { 69 | /* Ignore */ 70 | } 71 | 72 | /** 73 | * A reactive, fluid grid layout with draggable, resizable components. 74 | */ 75 | 76 | export default class ReactGridLayout extends React.Component { 77 | // TODO publish internal ReactClass displayName transform 78 | static displayName: ?string = "ReactGridLayout"; 79 | 80 | // Refactored to another module to make way for preval 81 | static propTypes = ReactGridLayoutPropTypes; 82 | 83 | static defaultProps: DefaultProps = { 84 | autoSize: true, 85 | cols: 12, 86 | className: "", 87 | style: {}, 88 | draggableHandle: "", 89 | draggableCancel: "", 90 | containerPadding: null, 91 | rowHeight: 150, 92 | maxRows: Infinity, // infinite vertical growth 93 | layout: [], 94 | margin: [10, 10], 95 | isBounded: false, 96 | isDraggable: true, 97 | isResizable: true, 98 | allowOverlap: false, 99 | isDroppable: false, 100 | useCSSTransforms: true, 101 | transformScale: 1, 102 | verticalCompact: true, 103 | compactType: "vertical", 104 | preventCollision: false, 105 | droppingItem: { 106 | i: "__dropping-elem__", 107 | h: 1, 108 | w: 1 109 | }, 110 | resizeHandles: ["se"], 111 | onLayoutChange: noop, 112 | onDragStart: noop, 113 | onDrag: noop, 114 | onDragStop: noop, 115 | onResizeStart: noop, 116 | onResize: noop, 117 | onResizeStop: noop, 118 | onDrop: noop, 119 | onDropDragOver: noop 120 | }; 121 | 122 | state: State = { 123 | activeDrag: null, 124 | layout: synchronizeLayoutWithChildren( 125 | this.props.layout, 126 | this.props.children, 127 | this.props.cols, 128 | // Legacy support for verticalCompact: false 129 | compactType(this.props), 130 | this.props.allowOverlap 131 | ), 132 | mounted: false, 133 | oldDragItem: null, 134 | oldLayout: null, 135 | oldResizeItem: null, 136 | resizing: false, 137 | droppingDOMNode: null, 138 | children: [] 139 | }; 140 | 141 | dragEnterCounter: number = 0; 142 | 143 | componentDidMount() { 144 | this.setState({ mounted: true }); 145 | // Possibly call back with layout on mount. This should be done after correcting the layout width 146 | // to ensure we don't rerender with the wrong width. 147 | this.onLayoutMaybeChanged(this.state.layout, this.props.layout); 148 | } 149 | 150 | static getDerivedStateFromProps( 151 | nextProps: Props, 152 | prevState: State 153 | ): $Shape | null { 154 | let newLayoutBase; 155 | 156 | if (prevState.activeDrag) { 157 | return null; 158 | } 159 | 160 | // Legacy support for compactType 161 | // Allow parent to set layout directly. 162 | if ( 163 | !deepEqual(nextProps.layout, prevState.propsLayout) || 164 | nextProps.compactType !== prevState.compactType 165 | ) { 166 | newLayoutBase = nextProps.layout; 167 | } else if (!childrenEqual(nextProps.children, prevState.children)) { 168 | // If children change, also regenerate the layout. Use our state 169 | // as the base in case because it may be more up to date than 170 | // what is in props. 171 | newLayoutBase = prevState.layout; 172 | } 173 | 174 | // We need to regenerate the layout. 175 | if (newLayoutBase) { 176 | const newLayout = synchronizeLayoutWithChildren( 177 | newLayoutBase, 178 | nextProps.children, 179 | nextProps.cols, 180 | compactType(nextProps), 181 | nextProps.allowOverlap 182 | ); 183 | 184 | return { 185 | layout: newLayout, 186 | // We need to save these props to state for using 187 | // getDerivedStateFromProps instead of componentDidMount (in which we would get extra rerender) 188 | compactType: nextProps.compactType, 189 | children: nextProps.children, 190 | propsLayout: nextProps.layout 191 | }; 192 | } 193 | 194 | return null; 195 | } 196 | 197 | shouldComponentUpdate(nextProps: Props, nextState: State): boolean { 198 | return ( 199 | // NOTE: this is almost always unequal. Therefore the only way to get better performance 200 | // from SCU is if the user intentionally memoizes children. If they do, and they can 201 | // handle changes properly, performance will increase. 202 | this.props.children !== nextProps.children || 203 | !fastRGLPropsEqual(this.props, nextProps, deepEqual) || 204 | this.state.activeDrag !== nextState.activeDrag || 205 | this.state.mounted !== nextState.mounted || 206 | this.state.droppingPosition !== nextState.droppingPosition 207 | ); 208 | } 209 | 210 | componentDidUpdate(prevProps: Props, prevState: State) { 211 | if (!this.state.activeDrag) { 212 | const newLayout = this.state.layout; 213 | const oldLayout = prevState.layout; 214 | 215 | this.onLayoutMaybeChanged(newLayout, oldLayout); 216 | } 217 | } 218 | 219 | /** 220 | * Calculates a pixel value for the container. 221 | * @return {String} Container height in pixels. 222 | */ 223 | containerHeight(): ?string { 224 | if (!this.props.autoSize) return; 225 | const nbRow = bottom(this.state.layout); 226 | const containerPaddingY = this.props.containerPadding 227 | ? this.props.containerPadding[1] 228 | : this.props.margin[1]; 229 | return ( 230 | nbRow * this.props.rowHeight + 231 | (nbRow - 1) * this.props.margin[1] + 232 | containerPaddingY * 2 + 233 | "px" 234 | ); 235 | } 236 | 237 | /** 238 | * When dragging starts 239 | * @param {String} i Id of the child 240 | * @param {Number} x X position of the move 241 | * @param {Number} y Y position of the move 242 | * @param {Event} e The mousedown event 243 | * @param {Element} node The current dragging DOM element 244 | */ 245 | onDragStart: (i: string, x: number, y: number, GridDragEvent) => void = ( 246 | i: string, 247 | x: number, 248 | y: number, 249 | { e, node }: GridDragEvent 250 | ) => { 251 | const { layout } = this.state; 252 | const l = getLayoutItem(layout, i); 253 | if (!l) return; 254 | 255 | // Create placeholder (display only) 256 | const placeholder = { 257 | w: l.w, 258 | h: l.h, 259 | x: l.x, 260 | y: l.y, 261 | placeholder: true, 262 | i: i 263 | }; 264 | 265 | this.setState({ 266 | oldDragItem: cloneLayoutItem(l), 267 | oldLayout: layout, 268 | activeDrag: placeholder 269 | }); 270 | 271 | return this.props.onDragStart(layout, l, l, null, e, node); 272 | }; 273 | 274 | /** 275 | * Each drag movement create a new dragelement and move the element to the dragged location 276 | * @param {String} i Id of the child 277 | * @param {Number} x X position of the move 278 | * @param {Number} y Y position of the move 279 | * @param {Event} e The mousedown event 280 | * @param {Element} node The current dragging DOM element 281 | */ 282 | onDrag: (i: string, x: number, y: number, GridDragEvent) => void = ( 283 | i, 284 | x, 285 | y, 286 | { e, node } 287 | ) => { 288 | const { oldDragItem } = this.state; 289 | let { layout } = this.state; 290 | const { cols, allowOverlap, preventCollision } = this.props; 291 | const l = getLayoutItem(layout, i); 292 | if (!l) return; 293 | 294 | // Create placeholder (display only) 295 | const placeholder = { 296 | w: l.w, 297 | h: l.h, 298 | x: l.x, 299 | y: l.y, 300 | placeholder: true, 301 | i: i 302 | }; 303 | 304 | // Move the element to the dragged location. 305 | const isUserAction = true; 306 | layout = moveElement( 307 | layout, 308 | l, 309 | x, 310 | y, 311 | isUserAction, 312 | preventCollision, 313 | compactType(this.props), 314 | cols, 315 | allowOverlap 316 | ); 317 | 318 | this.props.onDrag(layout, oldDragItem, l, placeholder, e, node); 319 | 320 | this.setState({ 321 | layout: allowOverlap 322 | ? layout 323 | : compact(layout, compactType(this.props), cols), 324 | activeDrag: placeholder 325 | }); 326 | }; 327 | 328 | /** 329 | * When dragging stops, figure out which position the element is closest to and update its x and y. 330 | * @param {String} i Index of the child. 331 | * @param {Number} x X position of the move 332 | * @param {Number} y Y position of the move 333 | * @param {Event} e The mousedown event 334 | * @param {Element} node The current dragging DOM element 335 | */ 336 | onDragStop: (i: string, x: number, y: number, GridDragEvent) => void = ( 337 | i, 338 | x, 339 | y, 340 | { e, node } 341 | ) => { 342 | if (!this.state.activeDrag) return; 343 | 344 | const { oldDragItem } = this.state; 345 | let { layout } = this.state; 346 | const { cols, preventCollision, allowOverlap } = this.props; 347 | const l = getLayoutItem(layout, i); 348 | if (!l) return; 349 | 350 | // Move the element here 351 | const isUserAction = true; 352 | layout = moveElement( 353 | layout, 354 | l, 355 | x, 356 | y, 357 | isUserAction, 358 | preventCollision, 359 | compactType(this.props), 360 | cols, 361 | allowOverlap 362 | ); 363 | 364 | // Set state 365 | const newLayout = allowOverlap 366 | ? layout 367 | : compact(layout, compactType(this.props), cols); 368 | 369 | this.props.onDragStop(newLayout, oldDragItem, l, null, e, node); 370 | 371 | const { oldLayout } = this.state; 372 | this.setState({ 373 | activeDrag: null, 374 | layout: newLayout, 375 | oldDragItem: null, 376 | oldLayout: null 377 | }); 378 | 379 | this.onLayoutMaybeChanged(newLayout, oldLayout); 380 | }; 381 | 382 | onLayoutMaybeChanged(newLayout: Layout, oldLayout: ?Layout) { 383 | if (!oldLayout) oldLayout = this.state.layout; 384 | 385 | if (!deepEqual(oldLayout, newLayout)) { 386 | this.props.onLayoutChange(newLayout); 387 | } 388 | } 389 | 390 | onResizeStart: (i: string, w: number, h: number, GridResizeEvent) => void = ( 391 | i, 392 | w, 393 | h, 394 | { e, node } 395 | ) => { 396 | const { layout } = this.state; 397 | const l = getLayoutItem(layout, i); 398 | if (!l) return; 399 | 400 | this.setState({ 401 | oldResizeItem: cloneLayoutItem(l), 402 | oldLayout: this.state.layout, 403 | resizing: true 404 | }); 405 | 406 | this.props.onResizeStart(layout, l, l, null, e, node); 407 | }; 408 | 409 | onResize: (i: string, w: number, h: number, GridResizeEvent) => void = ( 410 | i, 411 | w, 412 | h, 413 | { e, node, size, handle } 414 | ) => { 415 | const { oldResizeItem } = this.state; 416 | const { layout } = this.state; 417 | const { cols, preventCollision, allowOverlap } = this.props; 418 | 419 | let shouldMoveItem = false; 420 | let finalLayout; 421 | let x; 422 | let y; 423 | 424 | const [newLayout, l] = withLayoutItem(layout, i, l => { 425 | let hasCollisions; 426 | x = l.x; 427 | y = l.y; 428 | if (["sw", "w", "nw", "n", "ne"].indexOf(handle) !== -1) { 429 | if (["sw", "nw", "w"].indexOf(handle) !== -1) { 430 | x = l.x + (l.w - w); 431 | w = l.x !== x && x < 0 ? l.w : w; 432 | x = x < 0 ? 0 : x; 433 | } 434 | 435 | if (["ne", "n", "nw"].indexOf(handle) !== -1) { 436 | y = l.y + (l.h - h); 437 | h = l.y !== y && y < 0 ? l.h : h; 438 | y = y < 0 ? 0 : y; 439 | } 440 | 441 | shouldMoveItem = true; 442 | } 443 | 444 | // Something like quad tree should be used 445 | // to find collisions faster 446 | if (preventCollision && !allowOverlap) { 447 | const collisions = getAllCollisions(layout, { 448 | ...l, 449 | w, 450 | h, 451 | x, 452 | y 453 | }).filter(layoutItem => layoutItem.i !== l.i); 454 | hasCollisions = collisions.length > 0; 455 | 456 | // If we're colliding, we need adjust the placeholder. 457 | if (hasCollisions) { 458 | // Reset layoutItem dimensions if there were collisions 459 | y = l.y; 460 | h = l.h; 461 | x = l.x; 462 | w = l.w; 463 | shouldMoveItem = false; 464 | } 465 | } 466 | 467 | l.w = w; 468 | l.h = h; 469 | 470 | return l; 471 | }); 472 | 473 | // Shouldn't ever happen, but typechecking makes it necessary 474 | if (!l) return; 475 | 476 | finalLayout = newLayout; 477 | if (shouldMoveItem) { 478 | // Move the element to the new position. 479 | const isUserAction = true; 480 | finalLayout = moveElement( 481 | newLayout, 482 | l, 483 | x, 484 | y, 485 | isUserAction, 486 | this.props.preventCollision, 487 | compactType(this.props), 488 | cols, 489 | allowOverlap 490 | ); 491 | } 492 | 493 | // Create placeholder element (display only) 494 | const placeholder = { 495 | w: l.w, 496 | h: l.h, 497 | x: l.x, 498 | y: l.y, 499 | static: true, 500 | i: i 501 | }; 502 | 503 | this.props.onResize(finalLayout, oldResizeItem, l, placeholder, e, node); 504 | 505 | // Re-compact the newLayout and set the drag placeholder. 506 | this.setState({ 507 | layout: allowOverlap 508 | ? finalLayout 509 | : compact(finalLayout, compactType(this.props), cols), 510 | activeDrag: placeholder 511 | }); 512 | }; 513 | 514 | onResizeStop: (i: string, w: number, h: number, GridResizeEvent) => void = ( 515 | i, 516 | w, 517 | h, 518 | { e, node } 519 | ) => { 520 | const { layout, oldResizeItem } = this.state; 521 | const { cols, allowOverlap } = this.props; 522 | const l = getLayoutItem(layout, i); 523 | 524 | // Set state 525 | const newLayout = allowOverlap 526 | ? layout 527 | : compact(layout, compactType(this.props), cols); 528 | 529 | this.props.onResizeStop(newLayout, oldResizeItem, l, null, e, node); 530 | 531 | const { oldLayout } = this.state; 532 | this.setState({ 533 | activeDrag: null, 534 | layout: newLayout, 535 | oldResizeItem: null, 536 | oldLayout: null, 537 | resizing: false 538 | }); 539 | 540 | this.onLayoutMaybeChanged(newLayout, oldLayout); 541 | }; 542 | 543 | /** 544 | * Create a placeholder object. 545 | * @return {Element} Placeholder div. 546 | */ 547 | placeholder(): ?ReactElement { 548 | const { activeDrag } = this.state; 549 | if (!activeDrag) return null; 550 | const { 551 | width, 552 | cols, 553 | margin, 554 | containerPadding, 555 | rowHeight, 556 | maxRows, 557 | useCSSTransforms, 558 | transformScale 559 | } = this.props; 560 | 561 | // {...this.state.activeDrag} is pretty slow, actually 562 | return ( 563 | 584 |
585 | 586 | ); 587 | } 588 | 589 | /** 590 | * Given a grid item, set its style attributes & surround in a . 591 | * @param {Element} child React element. 592 | * @return {Element} Element wrapped in draggable and properly placed. 593 | */ 594 | processGridItem( 595 | child: ReactElement, 596 | isDroppingItem?: boolean 597 | ): ?ReactElement { 598 | if (!child || !child.key) return; 599 | const l = getLayoutItem(this.state.layout, String(child.key)); 600 | if (!l) return null; 601 | const { 602 | width, 603 | cols, 604 | margin, 605 | containerPadding, 606 | rowHeight, 607 | maxRows, 608 | isDraggable, 609 | isResizable, 610 | isBounded, 611 | useCSSTransforms, 612 | transformScale, 613 | draggableCancel, 614 | draggableHandle, 615 | resizeHandles, 616 | resizeHandle 617 | } = this.props; 618 | const { mounted, droppingPosition } = this.state; 619 | 620 | // Determine user manipulations possible. 621 | // If an item is static, it can't be manipulated by default. 622 | // Any properties defined directly on the grid item will take precedence. 623 | const draggable = 624 | typeof l.isDraggable === "boolean" 625 | ? l.isDraggable 626 | : !l.static && isDraggable; 627 | const resizable = 628 | typeof l.isResizable === "boolean" 629 | ? l.isResizable 630 | : !l.static && isResizable; 631 | const resizeHandlesOptions = l.resizeHandles || resizeHandles; 632 | 633 | // isBounded set on child if set on parent, and child is not explicitly false 634 | const bounded = draggable && isBounded && l.isBounded !== false; 635 | 636 | return ( 637 | 672 | {child} 673 | 674 | ); 675 | } 676 | 677 | // Called while dragging an element. Part of browser native drag/drop API. 678 | // Native event target might be the layout itself, or an element within the layout. 679 | onDragOver: DragOverEvent => void | false = e => { 680 | e.preventDefault(); // Prevent any browser native action 681 | e.stopPropagation(); 682 | 683 | // we should ignore events from layout's children in Firefox 684 | // to avoid unpredictable jumping of a dropping placeholder 685 | // FIXME remove this hack 686 | if ( 687 | isFirefox && 688 | // $FlowIgnore can't figure this out 689 | !e.nativeEvent.target?.classList.contains(layoutClassName) 690 | ) { 691 | return false; 692 | } 693 | 694 | const { 695 | droppingItem, 696 | onDropDragOver, 697 | margin, 698 | cols, 699 | rowHeight, 700 | maxRows, 701 | width, 702 | containerPadding, 703 | transformScale 704 | } = this.props; 705 | // Allow user to customize the dropping item or short-circuit the drop based on the results 706 | // of the `onDragOver(e: Event)` callback. 707 | const onDragOverResult = onDropDragOver?.(e); 708 | if (onDragOverResult === false) { 709 | if (this.state.droppingDOMNode) { 710 | this.removeDroppingPlaceholder(); 711 | } 712 | return false; 713 | } 714 | const finalDroppingItem = { ...droppingItem, ...onDragOverResult }; 715 | 716 | const { layout } = this.state; 717 | 718 | // $FlowIgnore missing def 719 | const gridRect = e.currentTarget.getBoundingClientRect(); // The grid's position in the viewport 720 | 721 | // Calculate the mouse position relative to the grid 722 | const layerX = e.clientX - gridRect.left; 723 | const layerY = e.clientY - gridRect.top; 724 | const droppingPosition = { 725 | left: layerX / transformScale, 726 | top: layerY / transformScale, 727 | e 728 | }; 729 | 730 | if (!this.state.droppingDOMNode) { 731 | const positionParams: PositionParams = { 732 | cols, 733 | margin, 734 | maxRows, 735 | rowHeight, 736 | containerWidth: width, 737 | containerPadding: containerPadding || margin 738 | }; 739 | 740 | const calculatedPosition = calcXY( 741 | positionParams, 742 | layerY, 743 | layerX, 744 | finalDroppingItem.w, 745 | finalDroppingItem.h 746 | ); 747 | 748 | this.setState({ 749 | droppingDOMNode:
, 750 | droppingPosition, 751 | layout: [ 752 | ...layout, 753 | { 754 | ...finalDroppingItem, 755 | x: calculatedPosition.x, 756 | y: calculatedPosition.y, 757 | static: false, 758 | isDraggable: true 759 | } 760 | ] 761 | }); 762 | } else if (this.state.droppingPosition) { 763 | const { left, top } = this.state.droppingPosition; 764 | const shouldUpdatePosition = left != layerX || top != layerY; 765 | if (shouldUpdatePosition) { 766 | this.setState({ droppingPosition }); 767 | } 768 | } 769 | }; 770 | 771 | removeDroppingPlaceholder: () => void = () => { 772 | const { droppingItem, cols } = this.props; 773 | const { layout } = this.state; 774 | 775 | const newLayout = compact( 776 | layout.filter(l => l.i !== droppingItem.i), 777 | compactType(this.props), 778 | cols, 779 | this.props.allowOverlap 780 | ); 781 | 782 | this.setState({ 783 | layout: newLayout, 784 | droppingDOMNode: null, 785 | activeDrag: null, 786 | droppingPosition: undefined 787 | }); 788 | }; 789 | 790 | onDragLeave: EventHandler = e => { 791 | e.preventDefault(); // Prevent any browser native action 792 | e.stopPropagation(); 793 | this.dragEnterCounter--; 794 | 795 | // onDragLeave can be triggered on each layout's child. 796 | // But we know that count of dragEnter and dragLeave events 797 | // will be balanced after leaving the layout's container 798 | // so we can increase and decrease count of dragEnter and 799 | // when it'll be equal to 0 we'll remove the placeholder 800 | if (this.dragEnterCounter === 0) { 801 | this.removeDroppingPlaceholder(); 802 | } 803 | }; 804 | 805 | onDragEnter: EventHandler = e => { 806 | e.preventDefault(); // Prevent any browser native action 807 | e.stopPropagation(); 808 | this.dragEnterCounter++; 809 | }; 810 | 811 | onDrop: EventHandler = (e: Event) => { 812 | e.preventDefault(); // Prevent any browser native action 813 | e.stopPropagation(); 814 | const { droppingItem } = this.props; 815 | const { layout } = this.state; 816 | const item = layout.find(l => l.i === droppingItem.i); 817 | 818 | // reset dragEnter counter on drop 819 | this.dragEnterCounter = 0; 820 | 821 | this.removeDroppingPlaceholder(); 822 | 823 | this.props.onDrop(layout, item, e); 824 | }; 825 | 826 | render(): React.Element<"div"> { 827 | const { className, style, isDroppable, innerRef } = this.props; 828 | 829 | const mergedClassName = clsx(layoutClassName, className); 830 | const mergedStyle = { 831 | height: this.containerHeight(), 832 | ...style 833 | }; 834 | 835 | return ( 836 |
845 | {React.Children.map(this.props.children, child => 846 | this.processGridItem(child) 847 | )} 848 | {isDroppable && 849 | this.state.droppingDOMNode && 850 | this.processGridItem(this.state.droppingDOMNode, true)} 851 | {this.placeholder()} 852 |
853 | ); 854 | } 855 | } 856 | -------------------------------------------------------------------------------- /lib/ReactGridLayoutPropTypes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import PropTypes from "prop-types"; 3 | import React from "react"; 4 | import type { 5 | Ref, 6 | ChildrenArray as ReactChildrenArray, 7 | Element as ReactElement 8 | } from "react"; 9 | import type { 10 | DragOverEvent, 11 | EventCallback, 12 | CompactType, 13 | Layout, 14 | LayoutItem, 15 | ResizeHandleAxis 16 | } from "./utils"; 17 | 18 | // util 19 | export type ReactRef = {| 20 | +current: T | null 21 | |}; 22 | 23 | export type ResizeHandle = 24 | | ReactElement 25 | | (( 26 | resizeHandleAxis: ResizeHandleAxis, 27 | ref: ReactRef 28 | ) => ReactElement); 29 | 30 | // Defines which resize handles should be rendered (default: 'se') 31 | // Allows for any combination of: 32 | // 's' - South handle (bottom-center) 33 | // 'w' - West handle (left-center) 34 | // 'e' - East handle (right-center) 35 | // 'n' - North handle (top-center) 36 | // 'sw' - Southwest handle (bottom-left) 37 | // 'nw' - Northwest handle (top-left) 38 | // 'se' - Southeast handle (bottom-right) 39 | // 'ne' - Northeast handle (top-right) 40 | export const resizeHandleAxesType: ReactPropsChainableTypeChecker = 41 | PropTypes.arrayOf( 42 | PropTypes.oneOf(["s", "w", "e", "n", "sw", "nw", "se", "ne"]) 43 | ); 44 | // Custom component for resize handles 45 | export const resizeHandleType: ReactPropsChainableTypeChecker = 46 | PropTypes.oneOfType([PropTypes.node, PropTypes.func]); 47 | 48 | export type Props = {| 49 | className: string, 50 | style: Object, 51 | width: number, 52 | autoSize: boolean, 53 | cols: number, 54 | draggableCancel: string, 55 | draggableHandle: string, 56 | verticalCompact: boolean, 57 | compactType: CompactType, 58 | layout: Layout, 59 | margin: [number, number], 60 | containerPadding: ?[number, number], 61 | rowHeight: number, 62 | maxRows: number, 63 | isBounded: boolean, 64 | isDraggable: boolean, 65 | isResizable: boolean, 66 | isDroppable: boolean, 67 | preventCollision: boolean, 68 | useCSSTransforms: boolean, 69 | transformScale: number, 70 | droppingItem: $Shape, 71 | resizeHandles: ResizeHandleAxis[], 72 | resizeHandle?: ResizeHandle, 73 | allowOverlap: boolean, 74 | 75 | // Callbacks 76 | onLayoutChange: Layout => void, 77 | onDrag: EventCallback, 78 | onDragStart: EventCallback, 79 | onDragStop: EventCallback, 80 | onResize: EventCallback, 81 | onResizeStart: EventCallback, 82 | onResizeStop: EventCallback, 83 | onDropDragOver: (e: DragOverEvent) => ?({| w?: number, h?: number |} | false), 84 | onDrop: (layout: Layout, item: ?LayoutItem, e: Event) => void, 85 | children: ReactChildrenArray>, 86 | innerRef?: Ref<"div"> 87 | |}; 88 | 89 | export type DefaultProps = $Diff< 90 | Props, 91 | { 92 | children: ReactChildrenArray>, 93 | width: number 94 | } 95 | >; 96 | 97 | export default { 98 | // 99 | // Basic props 100 | // 101 | className: PropTypes.string, 102 | style: PropTypes.object, 103 | 104 | // This can be set explicitly. If it is not set, it will automatically 105 | // be set to the container width. Note that resizes will *not* cause this to adjust. 106 | // If you need that behavior, use WidthProvider. 107 | width: PropTypes.number, 108 | 109 | // If true, the container height swells and contracts to fit contents 110 | autoSize: PropTypes.bool, 111 | // # of cols. 112 | cols: PropTypes.number, 113 | 114 | // A selector that will not be draggable. 115 | draggableCancel: PropTypes.string, 116 | // A selector for the draggable handler 117 | draggableHandle: PropTypes.string, 118 | 119 | // Deprecated 120 | verticalCompact: function (props: Props) { 121 | if ( 122 | props.verticalCompact === false && 123 | process.env.NODE_ENV !== "production" 124 | ) { 125 | console.warn( 126 | // eslint-disable-line no-console 127 | "`verticalCompact` on is deprecated and will be removed soon. " + 128 | 'Use `compactType`: "horizontal" | "vertical" | null.' 129 | ); 130 | } 131 | }, 132 | // Choose vertical or hotizontal compaction 133 | compactType: (PropTypes.oneOf([ 134 | "vertical", 135 | "horizontal" 136 | ]): ReactPropsChainableTypeChecker), 137 | 138 | // layout is an array of object with the format: 139 | // {x: Number, y: Number, w: Number, h: Number, i: String} 140 | layout: function (props: Props) { 141 | var layout = props.layout; 142 | // I hope you're setting the data-grid property on the grid items 143 | if (layout === undefined) return; 144 | require("./utils").validateLayout(layout, "layout"); 145 | }, 146 | 147 | // 148 | // Grid Dimensions 149 | // 150 | 151 | // Margin between items [x, y] in px 152 | margin: (PropTypes.arrayOf(PropTypes.number): ReactPropsChainableTypeChecker), 153 | // Padding inside the container [x, y] in px 154 | containerPadding: (PropTypes.arrayOf( 155 | PropTypes.number 156 | ): ReactPropsChainableTypeChecker), 157 | // Rows have a static height, but you can change this based on breakpoints if you like 158 | rowHeight: PropTypes.number, 159 | // Default Infinity, but you can specify a max here if you like. 160 | // Note that this isn't fully fleshed out and won't error if you specify a layout that 161 | // extends beyond the row capacity. It will, however, not allow users to drag/resize 162 | // an item past the barrier. They can push items beyond the barrier, though. 163 | // Intentionally not documented for this reason. 164 | maxRows: PropTypes.number, 165 | 166 | // 167 | // Flags 168 | // 169 | isBounded: PropTypes.bool, 170 | isDraggable: PropTypes.bool, 171 | isResizable: PropTypes.bool, 172 | // If true, grid can be placed one over the other. 173 | allowOverlap: PropTypes.bool, 174 | // If true, grid items won't change position when being dragged over. 175 | preventCollision: PropTypes.bool, 176 | // Use CSS transforms instead of top/left 177 | useCSSTransforms: PropTypes.bool, 178 | // parent layout transform scale 179 | transformScale: PropTypes.number, 180 | // If true, an external element can trigger onDrop callback with a specific grid position as a parameter 181 | isDroppable: PropTypes.bool, 182 | 183 | // Resize handle options 184 | resizeHandles: resizeHandleAxesType, 185 | resizeHandle: resizeHandleType, 186 | 187 | // 188 | // Callbacks 189 | // 190 | 191 | // Callback so you can save the layout. Calls after each drag & resize stops. 192 | onLayoutChange: PropTypes.func, 193 | 194 | // Calls when drag starts. Callback is of the signature (layout, oldItem, newItem, placeholder, e, ?node). 195 | // All callbacks below have the same signature. 'start' and 'stop' callbacks omit the 'placeholder'. 196 | onDragStart: PropTypes.func, 197 | // Calls on each drag movement. 198 | onDrag: PropTypes.func, 199 | // Calls when drag is complete. 200 | onDragStop: PropTypes.func, 201 | //Calls when resize starts. 202 | onResizeStart: PropTypes.func, 203 | // Calls when resize movement happens. 204 | onResize: PropTypes.func, 205 | // Calls when resize is complete. 206 | onResizeStop: PropTypes.func, 207 | // Calls when some element is dropped. 208 | onDrop: PropTypes.func, 209 | 210 | // 211 | // Other validations 212 | // 213 | 214 | droppingItem: (PropTypes.shape({ 215 | i: PropTypes.string.isRequired, 216 | w: PropTypes.number.isRequired, 217 | h: PropTypes.number.isRequired 218 | }): ReactPropsChainableTypeChecker), 219 | 220 | // Children must not have duplicate keys. 221 | children: function (props: Props, propName: string) { 222 | const children = props[propName]; 223 | 224 | // Check children keys for duplicates. Throw if found. 225 | const keys = {}; 226 | React.Children.forEach(children, function (child) { 227 | if (child?.key == null) return; 228 | if (keys[child.key]) { 229 | throw new Error( 230 | 'Duplicate child key "' + 231 | child.key + 232 | '" found! This will cause problems in ReactGridLayout.' 233 | ); 234 | } 235 | keys[child.key] = true; 236 | }); 237 | }, 238 | 239 | // Optional ref for getting a reference for the wrapping div. 240 | innerRef: PropTypes.any 241 | }; 242 | -------------------------------------------------------------------------------- /lib/ResponsiveReactGridLayout.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from "react"; 3 | import PropTypes from "prop-types"; 4 | import { deepEqual } from "fast-equals"; 5 | 6 | import { 7 | cloneLayout, 8 | synchronizeLayoutWithChildren, 9 | validateLayout, 10 | noop, 11 | type Layout, 12 | type Pick 13 | } from "./utils"; 14 | import { 15 | getBreakpointFromWidth, 16 | getColsFromBreakpoint, 17 | findOrGenerateResponsiveLayout, 18 | type ResponsiveLayout, 19 | type OnLayoutChangeCallback, 20 | type Breakpoints 21 | } from "./responsiveUtils"; 22 | import ReactGridLayout from "./ReactGridLayout"; 23 | 24 | // $FlowFixMe[method-unbinding] 25 | const type = obj => Object.prototype.toString.call(obj); 26 | 27 | /** 28 | * Get a value of margin or containerPadding. 29 | * 30 | * @param {Array | Object} param Margin | containerPadding, e.g. [10, 10] | {lg: [10, 10], ...}. 31 | * @param {String} breakpoint Breakpoint: lg, md, sm, xs and etc. 32 | * @return {Array} 33 | */ 34 | function getIndentationValue( 35 | param: { [key: string]: T } | T, 36 | breakpoint: string 37 | ): T { 38 | // $FlowIgnore TODO fix this typedef 39 | if (param == null) return null; 40 | // $FlowIgnore TODO fix this typedef 41 | return Array.isArray(param) ? param : param[breakpoint]; 42 | } 43 | 44 | type State = { 45 | layout: Layout, 46 | breakpoint: string, 47 | cols: number, 48 | layouts?: ResponsiveLayout 49 | }; 50 | 51 | type Props = {| 52 | ...React.ElementConfig, 53 | 54 | // Responsive config 55 | breakpoint?: ?Breakpoint, 56 | breakpoints: Breakpoints, 57 | cols: { [key: Breakpoint]: number }, 58 | layouts: ResponsiveLayout, 59 | width: number, 60 | margin: { [key: Breakpoint]: [number, number] } | [number, number], 61 | /* prettier-ignore */ 62 | containerPadding: { [key: Breakpoint]: ?[number, number] } | ?[number, number], 63 | 64 | // Callbacks 65 | onBreakpointChange: (Breakpoint, cols: number) => void, 66 | onLayoutChange: OnLayoutChangeCallback, 67 | onWidthChange: ( 68 | containerWidth: number, 69 | margin: [number, number], 70 | cols: number, 71 | containerPadding: ?[number, number] 72 | ) => void 73 | |}; 74 | 75 | type DefaultProps = Pick< 76 | Props<>, 77 | {| 78 | allowOverlap: 0, 79 | breakpoints: 0, 80 | cols: 0, 81 | containerPadding: 0, 82 | layouts: 0, 83 | margin: 0, 84 | onBreakpointChange: 0, 85 | onLayoutChange: 0, 86 | onWidthChange: 0 87 | |} 88 | >; 89 | 90 | export default class ResponsiveReactGridLayout extends React.Component< 91 | Props<>, 92 | State 93 | > { 94 | // This should only include propTypes needed in this code; RGL itself 95 | // will do validation of the rest props passed to it. 96 | static propTypes = { 97 | // 98 | // Basic props 99 | // 100 | 101 | // Optional, but if you are managing width yourself you may want to set the breakpoint 102 | // yourself as well. 103 | breakpoint: PropTypes.string, 104 | 105 | // {name: pxVal}, e.g. {lg: 1200, md: 996, sm: 768, xs: 480} 106 | breakpoints: PropTypes.object, 107 | 108 | allowOverlap: PropTypes.bool, 109 | 110 | // # of cols. This is a breakpoint -> cols map 111 | cols: PropTypes.object, 112 | 113 | // # of margin. This is a breakpoint -> margin map 114 | // e.g. { lg: [5, 5], md: [10, 10], sm: [15, 15] } 115 | // Margin between items [x, y] in px 116 | // e.g. [10, 10] 117 | margin: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), 118 | 119 | // # of containerPadding. This is a breakpoint -> containerPadding map 120 | // e.g. { lg: [5, 5], md: [10, 10], sm: [15, 15] } 121 | // Padding inside the container [x, y] in px 122 | // e.g. [10, 10] 123 | containerPadding: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), 124 | 125 | // layouts is an object mapping breakpoints to layouts. 126 | // e.g. {lg: Layout, md: Layout, ...} 127 | layouts(props: Props<>, propName: string) { 128 | if (type(props[propName]) !== "[object Object]") { 129 | throw new Error( 130 | "Layout property must be an object. Received: " + 131 | type(props[propName]) 132 | ); 133 | } 134 | Object.keys(props[propName]).forEach(key => { 135 | if (!(key in props.breakpoints)) { 136 | throw new Error( 137 | "Each key in layouts must align with a key in breakpoints." 138 | ); 139 | } 140 | validateLayout(props.layouts[key], "layouts." + key); 141 | }); 142 | }, 143 | 144 | // The width of this component. 145 | // Required in this propTypes stanza because generateInitialState() will fail without it. 146 | width: PropTypes.number.isRequired, 147 | 148 | // 149 | // Callbacks 150 | // 151 | 152 | // Calls back with breakpoint and new # cols 153 | onBreakpointChange: PropTypes.func, 154 | 155 | // Callback so you can save the layout. 156 | // Calls back with (currentLayout, allLayouts). allLayouts are keyed by breakpoint. 157 | onLayoutChange: PropTypes.func, 158 | 159 | // Calls back with (containerWidth, margin, cols, containerPadding) 160 | onWidthChange: PropTypes.func 161 | }; 162 | 163 | static defaultProps: DefaultProps = { 164 | breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }, 165 | cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }, 166 | containerPadding: { lg: null, md: null, sm: null, xs: null, xxs: null }, 167 | layouts: {}, 168 | margin: [10, 10], 169 | allowOverlap: false, 170 | onBreakpointChange: noop, 171 | onLayoutChange: noop, 172 | onWidthChange: noop 173 | }; 174 | 175 | state: State = this.generateInitialState(); 176 | 177 | generateInitialState(): State { 178 | const { width, breakpoints, layouts, cols } = this.props; 179 | const breakpoint = getBreakpointFromWidth(breakpoints, width); 180 | const colNo = getColsFromBreakpoint(breakpoint, cols); 181 | // verticalCompact compatibility, now deprecated 182 | const compactType = 183 | this.props.verticalCompact === false ? null : this.props.compactType; 184 | // Get the initial layout. This can tricky; we try to generate one however possible if one doesn't exist 185 | // for this layout. 186 | const initialLayout = findOrGenerateResponsiveLayout( 187 | layouts, 188 | breakpoints, 189 | breakpoint, 190 | breakpoint, 191 | colNo, 192 | compactType 193 | ); 194 | 195 | return { 196 | layout: initialLayout, 197 | breakpoint: breakpoint, 198 | cols: colNo 199 | }; 200 | } 201 | 202 | static getDerivedStateFromProps( 203 | nextProps: Props<*>, 204 | prevState: State 205 | ): ?$Shape { 206 | if (!deepEqual(nextProps.layouts, prevState.layouts)) { 207 | // Allow parent to set layouts directly. 208 | const { breakpoint, cols } = prevState; 209 | 210 | // Since we're setting an entirely new layout object, we must generate a new responsive layout 211 | // if one does not exist. 212 | const newLayout = findOrGenerateResponsiveLayout( 213 | nextProps.layouts, 214 | nextProps.breakpoints, 215 | breakpoint, 216 | breakpoint, 217 | cols, 218 | nextProps.compactType 219 | ); 220 | return { layout: newLayout, layouts: nextProps.layouts }; 221 | } 222 | 223 | return null; 224 | } 225 | 226 | componentDidUpdate(prevProps: Props<*>) { 227 | // Allow parent to set width or breakpoint directly. 228 | if ( 229 | this.props.width != prevProps.width || 230 | this.props.breakpoint !== prevProps.breakpoint || 231 | !deepEqual(this.props.breakpoints, prevProps.breakpoints) || 232 | !deepEqual(this.props.cols, prevProps.cols) 233 | ) { 234 | this.onWidthChange(prevProps); 235 | } 236 | } 237 | 238 | // wrap layouts so we do not need to pass layouts to child 239 | onLayoutChange: Layout => void = (layout: Layout) => { 240 | this.props.onLayoutChange(layout, { 241 | ...this.props.layouts, 242 | [this.state.breakpoint]: layout 243 | }); 244 | }; 245 | 246 | /** 247 | * When the width changes work through breakpoints and reset state with the new width & breakpoint. 248 | * Width changes are necessary to figure out the widget widths. 249 | */ 250 | onWidthChange(prevProps: Props<*>) { 251 | const { breakpoints, cols, layouts, compactType } = this.props; 252 | const newBreakpoint = 253 | this.props.breakpoint || 254 | getBreakpointFromWidth(this.props.breakpoints, this.props.width); 255 | 256 | const lastBreakpoint = this.state.breakpoint; 257 | const newCols: number = getColsFromBreakpoint(newBreakpoint, cols); 258 | const newLayouts = { ...layouts }; 259 | 260 | // Breakpoint change 261 | if ( 262 | lastBreakpoint !== newBreakpoint || 263 | prevProps.breakpoints !== breakpoints || 264 | prevProps.cols !== cols 265 | ) { 266 | // Preserve the current layout if the current breakpoint is not present in the next layouts. 267 | if (!(lastBreakpoint in newLayouts)) 268 | newLayouts[lastBreakpoint] = cloneLayout(this.state.layout); 269 | 270 | // Find or generate a new layout. 271 | let layout = findOrGenerateResponsiveLayout( 272 | newLayouts, 273 | breakpoints, 274 | newBreakpoint, 275 | lastBreakpoint, 276 | newCols, 277 | compactType 278 | ); 279 | 280 | // This adds missing items. 281 | layout = synchronizeLayoutWithChildren( 282 | layout, 283 | this.props.children, 284 | newCols, 285 | compactType, 286 | this.props.allowOverlap 287 | ); 288 | 289 | // Store the new layout. 290 | newLayouts[newBreakpoint] = layout; 291 | 292 | // callbacks 293 | this.props.onBreakpointChange(newBreakpoint, newCols); 294 | this.props.onLayoutChange(layout, newLayouts); 295 | 296 | this.setState({ 297 | breakpoint: newBreakpoint, 298 | layout: layout, 299 | cols: newCols 300 | }); 301 | } 302 | 303 | const margin = getIndentationValue(this.props.margin, newBreakpoint); 304 | const containerPadding = getIndentationValue( 305 | this.props.containerPadding, 306 | newBreakpoint 307 | ); 308 | 309 | //call onWidthChange on every change of width, not only on breakpoint changes 310 | this.props.onWidthChange( 311 | this.props.width, 312 | margin, 313 | newCols, 314 | containerPadding 315 | ); 316 | } 317 | 318 | render(): React.Element { 319 | /* eslint-disable no-unused-vars */ 320 | const { 321 | breakpoint, 322 | breakpoints, 323 | cols, 324 | layouts, 325 | margin, 326 | containerPadding, 327 | onBreakpointChange, 328 | onLayoutChange, 329 | onWidthChange, 330 | ...other 331 | } = this.props; 332 | /* eslint-enable no-unused-vars */ 333 | 334 | return ( 335 | 347 | ); 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /lib/calculateUtils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from "./utils"; 3 | 4 | export type PositionParams = { 5 | margin: [number, number], 6 | containerPadding: [number, number], 7 | containerWidth: number, 8 | cols: number, 9 | rowHeight: number, 10 | maxRows: number 11 | }; 12 | 13 | // Helper for generating column width 14 | export function calcGridColWidth(positionParams: PositionParams): number { 15 | const { margin, containerPadding, containerWidth, cols } = positionParams; 16 | return ( 17 | (containerWidth - margin[0] * (cols - 1) - containerPadding[0] * 2) / cols 18 | ); 19 | } 20 | 21 | // This can either be called: 22 | // calcGridItemWHPx(w, colWidth, margin[0]) 23 | // or 24 | // calcGridItemWHPx(h, rowHeight, margin[1]) 25 | export function calcGridItemWHPx( 26 | gridUnits: number, 27 | colOrRowSize: number, 28 | marginPx: number 29 | ): number { 30 | // 0 * Infinity === NaN, which causes problems with resize contraints 31 | if (!Number.isFinite(gridUnits)) return gridUnits; 32 | return Math.round( 33 | colOrRowSize * gridUnits + Math.max(0, gridUnits - 1) * marginPx 34 | ); 35 | } 36 | 37 | /** 38 | * Return position on the page given an x, y, w, h. 39 | * left, top, width, height are all in pixels. 40 | * @param {PositionParams} positionParams Parameters of grid needed for coordinates calculations. 41 | * @param {Number} x X coordinate in grid units. 42 | * @param {Number} y Y coordinate in grid units. 43 | * @param {Number} w W coordinate in grid units. 44 | * @param {Number} h H coordinate in grid units. 45 | * @return {Position} Object containing coords. 46 | */ 47 | export function calcGridItemPosition( 48 | positionParams: PositionParams, 49 | x: number, 50 | y: number, 51 | w: number, 52 | h: number, 53 | state: ?Object 54 | ): Position { 55 | const { margin, containerPadding, rowHeight } = positionParams; 56 | const colWidth = calcGridColWidth(positionParams); 57 | const out = {}; 58 | 59 | // If resizing, use the exact width and height as returned from resizing callbacks. 60 | if (state && state.resizing) { 61 | out.width = Math.round(state.resizing.width); 62 | out.height = Math.round(state.resizing.height); 63 | } 64 | // Otherwise, calculate from grid units. 65 | else { 66 | out.width = calcGridItemWHPx(w, colWidth, margin[0]); 67 | out.height = calcGridItemWHPx(h, rowHeight, margin[1]); 68 | } 69 | 70 | // If dragging, use the exact width and height as returned from dragging callbacks. 71 | if (state && state.dragging) { 72 | out.top = Math.round(state.dragging.top); 73 | out.left = Math.round(state.dragging.left); 74 | } else if ( 75 | state && 76 | state.resizing && 77 | typeof state.resizing.top === "number" && 78 | typeof state.resizing.left === "number" 79 | ) { 80 | out.top = Math.round(state.resizing.top); 81 | out.left = Math.round(state.resizing.left); 82 | } 83 | // Otherwise, calculate from grid units. 84 | else { 85 | out.top = Math.round((rowHeight + margin[1]) * y + containerPadding[1]); 86 | out.left = Math.round((colWidth + margin[0]) * x + containerPadding[0]); 87 | } 88 | 89 | return out; 90 | } 91 | 92 | /** 93 | * Translate x and y coordinates from pixels to grid units. 94 | * @param {PositionParams} positionParams Parameters of grid needed for coordinates calculations. 95 | * @param {Number} top Top position (relative to parent) in pixels. 96 | * @param {Number} left Left position (relative to parent) in pixels. 97 | * @param {Number} w W coordinate in grid units. 98 | * @param {Number} h H coordinate in grid units. 99 | * @return {Object} x and y in grid units. 100 | */ 101 | export function calcXY( 102 | positionParams: PositionParams, 103 | top: number, 104 | left: number, 105 | w: number, 106 | h: number 107 | ): { x: number, y: number } { 108 | const { margin, containerPadding, cols, rowHeight, maxRows } = positionParams; 109 | const colWidth = calcGridColWidth(positionParams); 110 | 111 | // left = containerPaddingX + x * (colWidth + marginX) 112 | // x * (colWidth + marginX) = left - containerPaddingX 113 | // x = (left - containerPaddingX) / (colWidth + marginX) 114 | let x = Math.round((left - containerPadding[0]) / (colWidth + margin[0])); 115 | let y = Math.round((top - containerPadding[1]) / (rowHeight + margin[1])); 116 | 117 | // Capping 118 | x = clamp(x, 0, cols - w); 119 | y = clamp(y, 0, maxRows - h); 120 | return { x, y }; 121 | } 122 | 123 | /** 124 | * Given a height and width in pixel values, calculate grid units. 125 | * @param {PositionParams} positionParams Parameters of grid needed for coordinates calcluations. 126 | * @param {Number} height Height in pixels. 127 | * @param {Number} width Width in pixels. 128 | * @param {Number} x X coordinate in grid units. 129 | * @param {Number} y Y coordinate in grid units. 130 | * @param {String} handle Resize Handle. 131 | * @return {Object} w, h as grid units. 132 | */ 133 | export function calcWH( 134 | positionParams: PositionParams, 135 | width: number, 136 | height: number, 137 | x: number, 138 | y: number, 139 | handle: string 140 | ): { w: number, h: number } { 141 | const { margin, maxRows, cols, rowHeight } = positionParams; 142 | const colWidth = calcGridColWidth(positionParams); 143 | 144 | // width = colWidth * w - (margin * (w - 1)) 145 | // ... 146 | // w = (width + margin) / (colWidth + margin) 147 | let w = Math.round((width + margin[0]) / (colWidth + margin[0])); 148 | let h = Math.round((height + margin[1]) / (rowHeight + margin[1])); 149 | 150 | // Capping 151 | let _w = clamp(w, 0, cols - x); 152 | let _h = clamp(h, 0, maxRows - y); 153 | if (["sw", "w", "nw"].indexOf(handle) !== -1) { 154 | _w = clamp(w, 0, cols); 155 | } 156 | if (["nw", "n", "ne"].indexOf(handle) !== -1) { 157 | _h = clamp(h, 0, maxRows); 158 | } 159 | return { w: _w, h: _h }; 160 | } 161 | 162 | // Similar to _.clamp 163 | export function clamp( 164 | num: number, 165 | lowerBound: number, 166 | upperBound: number 167 | ): number { 168 | return Math.max(Math.min(num, upperBound), lowerBound); 169 | } 170 | -------------------------------------------------------------------------------- /lib/components/WidthProvider.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from "react"; 3 | import PropTypes from "prop-types"; 4 | import ResizeObserver from "resize-observer-polyfill"; 5 | import clsx from "clsx"; 6 | import type { ReactRef } from "../ReactGridLayoutPropTypes"; 7 | 8 | type WPDefaultProps = {| 9 | measureBeforeMount: boolean 10 | |}; 11 | 12 | // eslint-disable-next-line no-unused-vars 13 | type WPProps = {| 14 | className?: string, 15 | style?: Object, 16 | ...WPDefaultProps 17 | |}; 18 | 19 | type WPState = {| 20 | width: number 21 | |}; 22 | 23 | type ComposedProps = {| 24 | ...Config, 25 | measureBeforeMount?: boolean, 26 | className?: string, 27 | style?: Object, 28 | width?: number 29 | |}; 30 | 31 | const layoutClassName = "react-grid-layout"; 32 | 33 | /* 34 | * A simple HOC that provides facility for listening to container resizes. 35 | * 36 | * The Flow type is pretty janky here. I can't just spread `WPProps` into this returned object - I wish I could - but it triggers 37 | * a flow bug of some sort that causes it to stop typechecking. 38 | */ 39 | export default function WidthProvideRGL( 40 | ComposedComponent: React.AbstractComponent 41 | ): React.AbstractComponent> { 42 | return class WidthProvider extends React.Component< 43 | ComposedProps, 44 | WPState 45 | > { 46 | static defaultProps: WPDefaultProps = { 47 | measureBeforeMount: false 48 | }; 49 | 50 | static propTypes = { 51 | // If true, will not render children until mounted. Useful for getting the exact width before 52 | // rendering, to prevent any unsightly resizing. 53 | measureBeforeMount: PropTypes.bool 54 | }; 55 | 56 | state: WPState = { 57 | width: 1280 58 | }; 59 | 60 | elementRef: ReactRef = React.createRef(); 61 | mounted: boolean = false; 62 | resizeObserver: ResizeObserver; 63 | 64 | componentDidMount() { 65 | this.mounted = true; 66 | this.resizeObserver = new ResizeObserver(entries => { 67 | const node = this.elementRef.current; 68 | if (node instanceof HTMLElement) { 69 | const width = entries[0].contentRect.width; 70 | this.setState({ width }); 71 | } 72 | }); 73 | const node = this.elementRef.current; 74 | if (node instanceof HTMLElement) { 75 | this.resizeObserver.observe(node); 76 | } 77 | } 78 | 79 | componentWillUnmount() { 80 | this.mounted = false; 81 | const node = this.elementRef.current; 82 | if (node instanceof HTMLElement) { 83 | this.resizeObserver.unobserve(node); 84 | } 85 | this.resizeObserver.disconnect(); 86 | } 87 | 88 | render() { 89 | const { measureBeforeMount, ...rest } = this.props; 90 | if (measureBeforeMount && !this.mounted) { 91 | return ( 92 |
98 | ); 99 | } 100 | 101 | return ( 102 | 107 | ); 108 | } 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /lib/fastRGLPropsEqual.js: -------------------------------------------------------------------------------- 1 | // @preval 2 | 3 | require("@babel/register"); 4 | 5 | // Fast way to compare RGL props in shouldComponentUpdate. 6 | // Generates the fastest possible comparison of the type: 7 | // function (a, b) { return a.className === b.className && a.style === b.style && ... } 8 | // This avoids enumerating keys, avoids us keeping our own key list, and can be very easily optimized. 9 | 10 | const PropTypes = require("prop-types"); 11 | const propTypes = require("./ReactGridLayoutPropTypes").default; 12 | const keys = Object.keys(propTypes); 13 | 14 | // Remove 'children' key as we don't want to compare it 15 | keys.splice(keys.indexOf("children"), 1); 16 | 17 | // Returns a code string indicating what to do here. 18 | // In most cases we want to do a simple equality comparison, 19 | // but we have some arrays and tuples and objects we want 20 | // to do a shallow comparison on. 21 | function getEqualType(key) { 22 | if ( 23 | [ 24 | PropTypes.number, 25 | PropTypes.bool, 26 | PropTypes.string, 27 | PropTypes.func 28 | ].includes(propTypes[key]) 29 | ) { 30 | return `(a.${key} === b.${key})`; 31 | } 32 | return `isEqualImpl(a.${key}, b.${key})`; 33 | } 34 | 35 | // Exports a function that compares a and b. `isEqualImpl` is a required 36 | // third prop, as we can't otherwise access it. 37 | module.exports = () => 38 | eval(` 39 | function fastRGLPropsEqual(a, b, isEqualImpl) { 40 | if (a === b) return true; 41 | return ( 42 | ${keys.map(getEqualType).join(" && ")} 43 | ); 44 | } 45 | fastRGLPropsEqual; 46 | `); 47 | -------------------------------------------------------------------------------- /lib/responsiveUtils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { cloneLayout, compact, correctBounds } from "./utils"; 4 | 5 | import type { CompactType, Layout } from "./utils"; 6 | 7 | export type Breakpoint = string; 8 | export type DefaultBreakpoints = "lg" | "md" | "sm" | "xs" | "xxs"; 9 | 10 | // + indicates read-only 11 | export type ResponsiveLayout = { 12 | +[breakpoint: T]: Layout 13 | }; 14 | export type Breakpoints = { 15 | +[breakpoint: T]: number 16 | }; 17 | 18 | export type OnLayoutChangeCallback = ( 19 | Layout, 20 | { [key: Breakpoint]: Layout } 21 | ) => void; 22 | 23 | /** 24 | * Given a width, find the highest breakpoint that matches is valid for it (width > breakpoint). 25 | * 26 | * @param {Object} breakpoints Breakpoints object (e.g. {lg: 1200, md: 960, ...}) 27 | * @param {Number} width Screen width. 28 | * @return {String} Highest breakpoint that is less than width. 29 | */ 30 | export function getBreakpointFromWidth( 31 | breakpoints: Breakpoints, 32 | width: number 33 | ): Breakpoint { 34 | const sorted = sortBreakpoints(breakpoints); 35 | let matching = sorted[0]; 36 | for (let i = 1, len = sorted.length; i < len; i++) { 37 | const breakpointName = sorted[i]; 38 | if (width > breakpoints[breakpointName]) matching = breakpointName; 39 | } 40 | return matching; 41 | } 42 | 43 | /** 44 | * Given a breakpoint, get the # of cols set for it. 45 | * @param {String} breakpoint Breakpoint name. 46 | * @param {Object} cols Map of breakpoints to cols. 47 | * @return {Number} Number of cols. 48 | */ 49 | export function getColsFromBreakpoint( 50 | breakpoint: Breakpoint, 51 | cols: Breakpoints 52 | ): number { 53 | if (!cols[breakpoint]) { 54 | throw new Error( 55 | "ResponsiveReactGridLayout: `cols` entry for breakpoint " + 56 | breakpoint + 57 | " is missing!" 58 | ); 59 | } 60 | return cols[breakpoint]; 61 | } 62 | 63 | /** 64 | * Given existing layouts and a new breakpoint, find or generate a new layout. 65 | * 66 | * This finds the layout above the new one and generates from it, if it exists. 67 | * 68 | * @param {Object} layouts Existing layouts. 69 | * @param {Array} breakpoints All breakpoints. 70 | * @param {String} breakpoint New breakpoint. 71 | * @param {String} breakpoint Last breakpoint (for fallback). 72 | * @param {Number} cols Column count at new breakpoint. 73 | * @param {Boolean} verticalCompact Whether or not to compact the layout 74 | * vertically. 75 | * @return {Array} New layout. 76 | */ 77 | export function findOrGenerateResponsiveLayout( 78 | layouts: ResponsiveLayout, 79 | breakpoints: Breakpoints, 80 | breakpoint: Breakpoint, 81 | lastBreakpoint: Breakpoint, 82 | cols: number, 83 | compactType: CompactType 84 | ): Layout { 85 | // If it already exists, just return it. 86 | if (layouts[breakpoint]) return cloneLayout(layouts[breakpoint]); 87 | // Find or generate the next layout 88 | let layout = layouts[lastBreakpoint]; 89 | const breakpointsSorted = sortBreakpoints(breakpoints); 90 | const breakpointsAbove = breakpointsSorted.slice( 91 | breakpointsSorted.indexOf(breakpoint) 92 | ); 93 | for (let i = 0, len = breakpointsAbove.length; i < len; i++) { 94 | const b = breakpointsAbove[i]; 95 | if (layouts[b]) { 96 | layout = layouts[b]; 97 | break; 98 | } 99 | } 100 | layout = cloneLayout(layout || []); // clone layout so we don't modify existing items 101 | return compact(correctBounds(layout, { cols: cols }), compactType, cols); 102 | } 103 | 104 | /** 105 | * Given breakpoints, return an array of breakpoints sorted by width. This is usually 106 | * e.g. ['xxs', 'xs', 'sm', ...] 107 | * 108 | * @param {Object} breakpoints Key/value pair of breakpoint names to widths. 109 | * @return {Array} Sorted breakpoints. 110 | */ 111 | export function sortBreakpoints( 112 | breakpoints: Breakpoints 113 | ): Array { 114 | const keys: Array = Object.keys(breakpoints); 115 | return keys.sort(function (a, b) { 116 | return breakpoints[a] - breakpoints[b]; 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /margin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemo-crypto/react-grid-layout/b29fce6d918dc85fb0f13509f82a8a93a9bc0370/margin.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid-layout", 3 | "version": "1.5.0", 4 | "description": "A draggable and resizable grid layout with responsive breakpoints, for React.", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "make lint", 8 | "test": "make test", 9 | "build": "make build", 10 | "build-example": "make build-example", 11 | "view-example": "make view-example", 12 | "dev": "make dev", 13 | "prepublishOnly": "make build", 14 | "validate": "npm ls", 15 | "flow": "flow", 16 | "fmt": "prettier --write .", 17 | "fmt:check": "prettier --check ." 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git@github.com:STRML/react-grid-layout.git" 22 | }, 23 | "keywords": [ 24 | "react", 25 | "grid", 26 | "drag", 27 | "draggable", 28 | "resize", 29 | "resizable", 30 | "fluid", 31 | "responsive" 32 | ], 33 | "author": "Samuel Reed (http://strml.net/)", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/STRML/react-grid-layout/issues" 37 | }, 38 | "homepage": "https://github.com/STRML/react-grid-layout", 39 | "dependencies": { 40 | "clsx": "^2.0.0", 41 | "fast-equals": "^4.0.3", 42 | "prop-types": "^15.8.1", 43 | "react-draggable": "^4.4.5", 44 | "react-resizable": "^3.0.5", 45 | "resize-observer-polyfill": "^1.5.1" 46 | }, 47 | "_dependencyNotes": { 48 | "fast-equals": "Bug in CRA5 causes fast-equals@5 to fail to import due to .cjs file. See https://github.com/react-grid-layout/react-grid-layout/issues/1904" 49 | }, 50 | "devDependencies": { 51 | "@babel/cli": "^7.23.4", 52 | "@babel/core": "^7.23.3", 53 | "@babel/eslint-parser": "^7.23.3", 54 | "@babel/plugin-proposal-class-properties": "^7.18.6", 55 | "@babel/plugin-transform-flow-comments": "^7.23.3", 56 | "@babel/preset-env": "^7.23.3", 57 | "@babel/preset-flow": "^7.23.3", 58 | "@babel/preset-react": "^7.23.3", 59 | "@babel/register": "^7.22.15", 60 | "@webpack-cli/serve": "^2.0.5", 61 | "babel-jest": "^29.7.0", 62 | "babel-loader": "^9.1.3", 63 | "babel-plugin-preval": "^5.1.0", 64 | "css-loader": "^6.8.1", 65 | "ejs": "^3.1.9", 66 | "enzyme": "^3.11.0", 67 | "enzyme-adapter-react-16": "^1.15.7", 68 | "enzyme-to-json": "^3.6.2", 69 | "eslint": "^8.54.0", 70 | "eslint-plugin-flowtype": "^8.0.3", 71 | "eslint-plugin-mocha": "^10.2.0", 72 | "eslint-plugin-react": "^7.33.2", 73 | "eslint-plugin-unicorn": "^49.0.0", 74 | "exports-loader": "^4.0.0", 75 | "flow-bin": "^0.172.0", 76 | "husky": "^8.0.3", 77 | "imports-loader": "^4.0.1", 78 | "jest": "^29.7.0", 79 | "jest-environment-jsdom": "^29.7.0", 80 | "lint-staged": "^15.1.0", 81 | "lodash": "^4.17.21", 82 | "opener": "^1.5.2", 83 | "prettier": "^3.1.0", 84 | "react": "^16.13.1", 85 | "react-dom": "^16.13.1", 86 | "react-hot-loader": "^4.13.1", 87 | "react-transform-hmr": "^1.0.2", 88 | "style-loader": "^3.3.3", 89 | "timsort": "^0.3.0", 90 | "webpack": "^5.89.0", 91 | "webpack-cli": "^5.1.4", 92 | "webpack-dev-server": "^4.15.1" 93 | }, 94 | "peerDependencies": { 95 | "react": ">= 16.3.0", 96 | "react-dom": ">= 16.3.0" 97 | }, 98 | "publishConfig": { 99 | "registry": "https://registry.npmjs.org" 100 | }, 101 | "jest": { 102 | "setupFilesAfterEnv": [ 103 | "test/util/setupTests.js" 104 | ], 105 | "snapshotSerializers": [ 106 | "enzyme-to-json/serializer" 107 | ], 108 | "testMatch": [ 109 | "/test/spec/*.js" 110 | ], 111 | "testEnvironment": "jsdom", 112 | "testEnvironmentOptions": { 113 | "url": "http://localhost" 114 | }, 115 | "coverageThreshold": { 116 | "global": { 117 | "statements": 77, 118 | "branches": 72, 119 | "functions": 77, 120 | "lines": 78 121 | } 122 | } 123 | }, 124 | "lint-staged": { 125 | "*.{js,jsx}": [ 126 | "eslint --ext .js,.jsx --fix" 127 | ], 128 | "*": [ 129 | "prettier --ignore-unknown --write" 130 | ] 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /test/dev-hook.jsx: -------------------------------------------------------------------------------- 1 | import "react-hot-loader"; 2 | import { hot } from "react-hot-loader/root"; 3 | import DevLayout from "./examples/00-showcase.jsx"; 4 | import makeLayout from "./test-hook"; 5 | 6 | const Layout = makeLayout(DevLayout); 7 | 8 | export default hot(Layout); 9 | -------------------------------------------------------------------------------- /test/examples/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "react/prop-types": 0 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/examples/00-showcase.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from "react"; 3 | import _ from "lodash"; 4 | import Responsive from '../../lib/ResponsiveReactGridLayout'; 5 | import WidthProvider from '../../lib/components/WidthProvider'; 6 | import type {CompactType, Layout, LayoutItem, ReactChildren} from '../../lib/utils'; 7 | import type {Breakpoint, OnLayoutChangeCallback} from '../../lib/responsiveUtils'; 8 | const ResponsiveReactGridLayout = WidthProvider(Responsive); 9 | 10 | type Props = {| 11 | className: string, 12 | cols: {[string]: number}, 13 | onLayoutChange: Function, 14 | rowHeight: number, 15 | |}; 16 | type State = {| 17 | currentBreakpoint: string, 18 | compactType: CompactType, 19 | mounted: boolean, 20 | resizeHandles: string[], 21 | layouts: {[string]: Layout} 22 | |}; 23 | 24 | const availableHandles = ["s", "w", "e", "n", "sw", "nw", "se", "ne"]; 25 | 26 | export default class ShowcaseLayout extends React.Component { 27 | static defaultProps: Props = { 28 | className: "layout", 29 | rowHeight: 30, 30 | onLayoutChange: function() {}, 31 | cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }, 32 | }; 33 | 34 | state: State = { 35 | currentBreakpoint: "lg", 36 | compactType: "vertical", 37 | resizeHandles: ['se'], 38 | mounted: false, 39 | layouts: { lg: generateLayout(['se']) } 40 | }; 41 | 42 | componentDidMount() { 43 | this.setState({ mounted: true }); 44 | } 45 | 46 | generateDOM(): ReactChildren { 47 | return _.map(this.state.layouts.lg, function(l, i) { 48 | return ( 49 |
50 | {l.static ? ( 51 | 55 | Static - {i} 56 | 57 | ) : ( 58 | {i} 59 | )} 60 |
61 | ); 62 | }); 63 | } 64 | 65 | onBreakpointChange: (Breakpoint) => void = (breakpoint) => { 66 | this.setState({ 67 | currentBreakpoint: breakpoint 68 | }); 69 | }; 70 | 71 | onCompactTypeChange: () => void = () => { 72 | const { compactType: oldCompactType } = this.state; 73 | const compactType = 74 | oldCompactType === "horizontal" 75 | ? "vertical" 76 | : oldCompactType === "vertical" 77 | ? null 78 | : "horizontal"; 79 | this.setState({ compactType }); 80 | }; 81 | 82 | onResizeTypeChange: () => void = () => { 83 | const resizeHandles = this.state.resizeHandles === availableHandles ? ['se'] : availableHandles; 84 | this.setState({resizeHandles, layouts: {lg: generateLayout(resizeHandles)}}); 85 | }; 86 | 87 | 88 | onLayoutChange: OnLayoutChangeCallback = (layout, layouts) => { 89 | this.props.onLayoutChange(layout, layouts); 90 | }; 91 | 92 | onNewLayout: EventHandler = () => { 93 | this.setState({ 94 | layouts: { lg: generateLayout(this.state.resizeHandles) } 95 | }); 96 | }; 97 | 98 | onDrop: (layout: Layout, item: ?LayoutItem, e: Event) => void = (elemParams) => { 99 | alert(`Element parameters: ${JSON.stringify(elemParams)}`); 100 | }; 101 | 102 | render(): React.Node { 103 | // eslint-disable-next-line no-unused-vars 104 | return ( 105 |
106 |
107 | Current Breakpoint: {this.state.currentBreakpoint} ( 108 | {this.props.cols[this.state.currentBreakpoint]} columns) 109 |
110 |
111 | Compaction type:{" "} 112 | {_.capitalize(this.state.compactType) || "No Compaction"} 113 |
114 | 115 | 118 | 121 | 135 | {this.generateDOM()} 136 | 137 |
138 | ); 139 | } 140 | } 141 | 142 | function generateLayout(resizeHandles) { 143 | return _.map(_.range(0, 25), function(item, i) { 144 | var y = Math.ceil(Math.random() * 4) + 1; 145 | return { 146 | x: Math.round(Math.random() * 5) * 2, 147 | y: Math.floor(i / 6) * y, 148 | w: 2, 149 | h: y, 150 | i: i.toString(), 151 | static: Math.random() < 0.05, 152 | resizeHandles 153 | }; 154 | }); 155 | } 156 | 157 | if (process.env.STATIC_EXAMPLES === true) { 158 | import("../test-hook.jsx").then(fn => fn.default(ShowcaseLayout)); 159 | } 160 | -------------------------------------------------------------------------------- /test/examples/01-basic.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | export default class BasicLayout extends React.PureComponent { 8 | static defaultProps = { 9 | className: "layout", 10 | items: 20, 11 | rowHeight: 30, 12 | onLayoutChange: function() {}, 13 | cols: 12 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | const layout = this.generateLayout(); 20 | this.state = { layout }; 21 | } 22 | 23 | generateDOM() { 24 | return _.map(_.range(this.props.items), function(i) { 25 | return ( 26 |
27 | {i} 28 |
29 | ); 30 | }); 31 | } 32 | 33 | generateLayout() { 34 | const p = this.props; 35 | return _.map(new Array(p.items), function(item, i) { 36 | const y = _.result(p, "y") || Math.ceil(Math.random() * 4) + 1; 37 | return { 38 | x: (i * 2) % 12, 39 | y: Math.floor(i / 6) * y, 40 | w: 2, 41 | h: y, 42 | i: i.toString() 43 | }; 44 | }); 45 | } 46 | 47 | onLayoutChange(layout) { 48 | this.props.onLayoutChange(layout); 49 | } 50 | 51 | render() { 52 | return ( 53 | 58 | {this.generateDOM()} 59 | 60 | ); 61 | } 62 | } 63 | 64 | if (process.env.STATIC_EXAMPLES === true) { 65 | import("../test-hook.jsx").then(fn => fn.default(BasicLayout)); 66 | } 67 | -------------------------------------------------------------------------------- /test/examples/02-no-dragging.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | export default class NoDraggingLayout extends React.PureComponent { 8 | static defaultProps = { 9 | className: "layout", 10 | isDraggable: false, 11 | isResizable: false, 12 | items: 50, 13 | cols: 12, 14 | rowHeight: 30, 15 | onLayoutChange: function() {} 16 | }; 17 | 18 | constructor(props) { 19 | super(props); 20 | 21 | const layout = this.generateLayout(); 22 | this.state = { layout }; 23 | } 24 | 25 | generateDOM() { 26 | return _.map(_.range(this.props.items), function(i) { 27 | return ( 28 |
29 | {i} 30 |
31 | ); 32 | }); 33 | } 34 | 35 | generateLayout() { 36 | const p = this.props; 37 | return _.map(new Array(p.items), function(item, i) { 38 | var y = _.result(p, "y") || Math.ceil(Math.random() * 4) + 1; 39 | return { 40 | x: (i * 2) % 12, 41 | y: Math.floor(i / 6) * y, 42 | w: 2, 43 | h: y, 44 | i: i.toString() 45 | }; 46 | }); 47 | } 48 | 49 | onLayoutChange(layout) { 50 | this.props.onLayoutChange(layout); 51 | } 52 | 53 | render() { 54 | return ( 55 | 60 | {this.generateDOM()} 61 | 62 | ); 63 | } 64 | } 65 | 66 | if (process.env.STATIC_EXAMPLES === true) { 67 | import("../test-hook.jsx").then(fn => fn.default(NoDraggingLayout)); 68 | } 69 | -------------------------------------------------------------------------------- /test/examples/03-messy.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from "react"; 3 | import _ from "lodash"; 4 | import RGL from '../../lib/ReactGridLayout'; 5 | import WidthProvider from '../../lib/components/WidthProvider'; 6 | import type {Layout, ReactChildren} from '../../lib/utils'; 7 | 8 | const ReactGridLayout = WidthProvider(RGL); 9 | 10 | type Props = {| 11 | className: string, 12 | cols: number, 13 | items: number, 14 | onLayoutChange: Function, 15 | rowHeight: number, 16 | |}; 17 | type State = {| 18 | layout: Layout 19 | |}; 20 | 21 | export default class MessyLayout extends React.PureComponent { 22 | static defaultProps: Props = { 23 | className: "layout", 24 | cols: 12, 25 | items: 20, 26 | onLayoutChange: function() {}, 27 | rowHeight: 30, 28 | }; 29 | 30 | state: State = { 31 | layout: this.generateLayout() 32 | }; 33 | 34 | generateDOM(): ReactChildren { 35 | return _.map(_.range(this.props.items), function(i) { 36 | return ( 37 |
38 | {i} 39 |
40 | ); 41 | }); 42 | } 43 | 44 | generateLayout(): Layout { 45 | const p = this.props; 46 | return _.map(new Array(p.items), function(item, i) { 47 | const w = Math.ceil(Math.random() * 4); 48 | const y = Math.ceil(Math.random() * 4) + 1; 49 | return { 50 | x: (i * 2) % 12, 51 | y: Math.floor(i / 6) * y, 52 | w: w, 53 | h: y, 54 | i: i.toString() 55 | }; 56 | }); 57 | } 58 | 59 | onLayoutChange: (Layout) => void = (layout: Layout) => { 60 | this.props.onLayoutChange(layout); 61 | }; 62 | 63 | render(): React.Node { 64 | // eslint-disable-next-line no-unused-vars 65 | const {items, ...props} = this.props; 66 | return ( 67 | 72 | {this.generateDOM()} 73 | 74 | ); 75 | } 76 | } 77 | 78 | if (process.env.STATIC_EXAMPLES === true) { 79 | import("../test-hook.jsx").then(fn => fn.default(MessyLayout)); 80 | } 81 | -------------------------------------------------------------------------------- /test/examples/04-grid-property.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | export default class GridPropertyLayout extends React.PureComponent { 8 | static defaultProps = { 9 | isDraggable: true, 10 | isResizable: true, 11 | items: 20, 12 | rowHeight: 30, 13 | onLayoutChange: function() {}, 14 | cols: 12 15 | }; 16 | 17 | generateDOM() { 18 | // Generate items with properties from the layout, rather than pass the layout directly 19 | const layout = this.generateLayout(); 20 | return _.map(_.range(this.props.items), function(i) { 21 | return ( 22 |
23 | {i} 24 |
25 | ); 26 | }); 27 | } 28 | 29 | generateLayout() { 30 | const p = this.props; 31 | return _.map(new Array(p.items), function(item, i) { 32 | var w = _.result(p, "w") || Math.ceil(Math.random() * 4); 33 | var y = _.result(p, "y") || Math.ceil(Math.random() * 4) + 1; 34 | return { 35 | x: (i * 2) % 12, 36 | y: Math.floor(i / 6) * y, 37 | w: w, 38 | h: y, 39 | i: i.toString() 40 | }; 41 | }); 42 | } 43 | 44 | onLayoutChange(layout) { 45 | this.props.onLayoutChange(layout); 46 | } 47 | 48 | render() { 49 | return ( 50 | 51 | {this.generateDOM()} 52 | 53 | ); 54 | } 55 | } 56 | 57 | if (process.env.STATIC_EXAMPLES === true) { 58 | import("../test-hook.jsx").then(fn => fn.default(GridPropertyLayout)); 59 | } 60 | -------------------------------------------------------------------------------- /test/examples/05-static-elements.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RGL, { WidthProvider } from "react-grid-layout"; 3 | 4 | const ReactGridLayout = WidthProvider(RGL); 5 | 6 | /** 7 | * This layout demonstrates how to use static grid elements. 8 | * Static elements are not draggable or resizable, and cannot be moved. 9 | */ 10 | export default class StaticElementsLayout extends React.PureComponent { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.onLayoutChange = this.onLayoutChange.bind(this); 15 | } 16 | 17 | onLayoutChange(layout) { 18 | this.props.onLayoutChange(layout); 19 | } 20 | 21 | render() { 22 | return ( 23 | 29 |
30 | 1 31 |
32 |
33 | 2 - Static 34 |
35 |
36 | 3 37 |
38 |
47 | 48 | 4 - Draggable with Handle 49 |
50 |
51 | [DRAG HERE] 52 |
53 |
54 |
55 |
56 |
57 | ); 58 | } 59 | } 60 | 61 | if (process.env.STATIC_EXAMPLES === true) { 62 | import("../test-hook.jsx").then(fn => fn.default(StaticElementsLayout)); 63 | } 64 | -------------------------------------------------------------------------------- /test/examples/06-dynamic-add-remove.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { WidthProvider, Responsive } from "react-grid-layout"; 3 | import _ from "lodash"; 4 | const ResponsiveReactGridLayout = WidthProvider(Responsive); 5 | 6 | /** 7 | * This layout demonstrates how to use a grid with a dynamic number of elements. 8 | */ 9 | export default class AddRemoveLayout extends React.PureComponent { 10 | static defaultProps = { 11 | className: "layout", 12 | cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }, 13 | rowHeight: 100 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | this.state = { 20 | items: [0, 1, 2, 3, 4].map(function(i, key, list) { 21 | return { 22 | i: i.toString(), 23 | x: i * 2, 24 | y: 0, 25 | w: 2, 26 | h: 2, 27 | add: i === (list.length - 1) 28 | }; 29 | }), 30 | newCounter: 0 31 | }; 32 | 33 | this.onAddItem = this.onAddItem.bind(this); 34 | this.onBreakpointChange = this.onBreakpointChange.bind(this); 35 | } 36 | 37 | createElement(el) { 38 | const removeStyle = { 39 | position: "absolute", 40 | right: "2px", 41 | top: 0, 42 | cursor: "pointer" 43 | }; 44 | const i = el.add ? "+" : el.i; 45 | return ( 46 |
47 | {el.add ? ( 48 | 53 | Add + 54 | 55 | ) : ( 56 | {i} 57 | )} 58 | 63 | x 64 | 65 |
66 | ); 67 | } 68 | 69 | onAddItem() { 70 | /*eslint no-console: 0*/ 71 | console.log("adding", "n" + this.state.newCounter); 72 | this.setState({ 73 | // Add a new item. It must have a unique key! 74 | items: this.state.items.concat({ 75 | i: "n" + this.state.newCounter, 76 | x: (this.state.items.length * 2) % (this.state.cols || 12), 77 | y: Infinity, // puts it at the bottom 78 | w: 2, 79 | h: 2 80 | }), 81 | // Increment the counter to ensure key is always unique. 82 | newCounter: this.state.newCounter + 1 83 | }); 84 | } 85 | 86 | // We're using the cols coming back from this to calculate where to add new items. 87 | onBreakpointChange(breakpoint, cols) { 88 | this.setState({ 89 | breakpoint: breakpoint, 90 | cols: cols 91 | }); 92 | } 93 | 94 | onLayoutChange(layout) { 95 | this.props.onLayoutChange(layout); 96 | this.setState({ layout: layout }); 97 | } 98 | 99 | onRemoveItem(i) { 100 | console.log("removing", i); 101 | this.setState({ items: _.reject(this.state.items, { i: i }) }); 102 | } 103 | 104 | render() { 105 | return ( 106 |
107 | 108 | 113 | {_.map(this.state.items, el => this.createElement(el))} 114 | 115 |
116 | ); 117 | } 118 | } 119 | 120 | if (process.env.STATIC_EXAMPLES === true) { 121 | import("../test-hook.jsx").then(fn => fn.default(AddRemoveLayout)); 122 | } 123 | -------------------------------------------------------------------------------- /test/examples/07-localstorage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RGL, { WidthProvider } from "react-grid-layout"; 3 | 4 | const ReactGridLayout = WidthProvider(RGL); 5 | const originalLayout = getFromLS("layout") || []; 6 | /** 7 | * This layout demonstrates how to sync to localstorage. 8 | */ 9 | export default class LocalStorageLayout extends React.PureComponent { 10 | static defaultProps = { 11 | className: "layout", 12 | cols: 12, 13 | rowHeight: 30, 14 | onLayoutChange: function() {} 15 | }; 16 | 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | layout: JSON.parse(JSON.stringify(originalLayout)) 22 | }; 23 | 24 | this.onLayoutChange = this.onLayoutChange.bind(this); 25 | this.resetLayout = this.resetLayout.bind(this); 26 | } 27 | 28 | resetLayout() { 29 | this.setState({ 30 | layout: [] 31 | }); 32 | } 33 | 34 | onLayoutChange(layout) { 35 | /*eslint no-console: 0*/ 36 | saveToLS("layout", layout); 37 | this.setState({ layout }); 38 | this.props.onLayoutChange(layout); // updates status display 39 | } 40 | 41 | render() { 42 | return ( 43 |
44 | 45 | 50 |
51 | 1 52 |
53 |
54 | 2 55 |
56 |
57 | 3 58 |
59 |
60 | 4 61 |
62 |
63 | 5 64 |
65 |
66 |
67 | ); 68 | } 69 | } 70 | 71 | function getFromLS(key) { 72 | let ls = {}; 73 | if (global.localStorage) { 74 | try { 75 | ls = JSON.parse(global.localStorage.getItem("rgl-7")) || {}; 76 | } catch (e) { 77 | /*Ignore*/ 78 | } 79 | } 80 | return ls[key]; 81 | } 82 | 83 | function saveToLS(key, value) { 84 | if (global.localStorage) { 85 | global.localStorage.setItem( 86 | "rgl-7", 87 | JSON.stringify({ 88 | [key]: value 89 | }) 90 | ); 91 | } 92 | } 93 | 94 | if (process.env.STATIC_EXAMPLES === true) { 95 | import("../test-hook.jsx").then(fn => fn.default(LocalStorageLayout)); 96 | } 97 | -------------------------------------------------------------------------------- /test/examples/08-localstorage-responsive.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { WidthProvider, Responsive } from "react-grid-layout"; 3 | 4 | const ResponsiveReactGridLayout = WidthProvider(Responsive); 5 | const originalLayouts = getFromLS("layouts") || {}; 6 | 7 | /** 8 | * This layout demonstrates how to sync multiple responsive layouts to localstorage. 9 | */ 10 | export default class ResponsiveLocalStorageLayout extends React.PureComponent { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | layouts: JSON.parse(JSON.stringify(originalLayouts)) 16 | }; 17 | } 18 | 19 | static get defaultProps() { 20 | return { 21 | className: "layout", 22 | cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }, 23 | rowHeight: 30 24 | }; 25 | } 26 | 27 | resetLayout() { 28 | this.setState({ layouts: {} }); 29 | } 30 | 31 | onLayoutChange(layout, layouts) { 32 | saveToLS("layouts", layouts); 33 | this.setState({ layouts }); 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 | 40 | 46 | this.onLayoutChange(layout, layouts) 47 | } 48 | > 49 |
50 | 1 51 |
52 |
53 | 2 54 |
55 |
56 | 3 57 |
58 |
59 | 4 60 |
61 |
62 | 5 63 |
64 |
65 |
66 | ); 67 | } 68 | } 69 | 70 | function getFromLS(key) { 71 | let ls = {}; 72 | if (global.localStorage) { 73 | try { 74 | ls = JSON.parse(global.localStorage.getItem("rgl-8")) || {}; 75 | } catch (e) { 76 | /*Ignore*/ 77 | } 78 | } 79 | return ls[key]; 80 | } 81 | 82 | function saveToLS(key, value) { 83 | if (global.localStorage) { 84 | global.localStorage.setItem( 85 | "rgl-8", 86 | JSON.stringify({ 87 | [key]: value 88 | }) 89 | ); 90 | } 91 | } 92 | 93 | if (process.env.STATIC_EXAMPLES === true) { 94 | import("../test-hook.jsx").then(fn => 95 | fn.default(ResponsiveLocalStorageLayout) 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /test/examples/09-min-max-wh.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | export default class MinMaxLayout extends React.PureComponent { 8 | static defaultProps = { 9 | isDraggable: true, 10 | isResizable: true, 11 | items: 20, 12 | rowHeight: 30, 13 | onLayoutChange: function() {}, 14 | cols: 12 15 | }; 16 | 17 | generateDOM() { 18 | // Generate items with properties from the layout, rather than pass the layout directly 19 | const layout = this.generateLayout(); 20 | return _.map(layout, function(l) { 21 | const mins = [l.minW, l.minH], 22 | maxes = [l.maxW, l.maxH]; 23 | return ( 24 |
25 | {l.i} 26 |
{"min:" + mins + " - max:" + maxes}
27 |
28 | ); 29 | }); 30 | } 31 | 32 | generateLayout() { 33 | const p = this.props; 34 | return _.map(new Array(p.items), function(item, i) { 35 | const minW = _.random(1, 6), 36 | minH = _.random(1, 6); 37 | const maxW = _.random(minW, 6), 38 | maxH = _.random(minH, 6); 39 | const w = _.random(minW, maxW); 40 | const y = _.random(minH, maxH); 41 | return { 42 | x: (i * 2) % 12, 43 | y: Math.floor(i / 6) * y, 44 | w, 45 | h: y, 46 | i: i.toString(), 47 | minW, 48 | maxW, 49 | minH, 50 | maxH 51 | }; 52 | }); 53 | } 54 | 55 | onLayoutChange(layout) { 56 | this.props.onLayoutChange(layout); 57 | } 58 | 59 | render() { 60 | return ( 61 | 62 | {this.generateDOM()} 63 | 64 | ); 65 | } 66 | } 67 | 68 | if (process.env.STATIC_EXAMPLES === true) { 69 | import("../test-hook.jsx").then(fn => fn.default(MinMaxLayout)); 70 | } 71 | -------------------------------------------------------------------------------- /test/examples/10-dynamic-min-max-wh.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | /** 8 | * This layout demonstrates how to use the `onResize` handler to enforce a min/max width and height. 9 | * 10 | * In this grid, all elements are allowed a max width of 2 if the height < 3, 11 | * and a min width of 2 if the height >= 3. 12 | */ 13 | export default class DynamicMinMaxLayout extends React.PureComponent { 14 | static defaultProps = { 15 | isDraggable: true, 16 | isResizable: true, 17 | items: 20, 18 | rowHeight: 30, 19 | onLayoutChange: function() {}, 20 | cols: 12 21 | }; 22 | 23 | generateDOM() { 24 | // Generate items with properties from the layout, rather than pass the layout directly 25 | const layout = this.generateLayout(); 26 | return _.map(layout, function(l) { 27 | return ( 28 |
29 | {l.i} 30 |
31 | ); 32 | }); 33 | } 34 | 35 | generateLayout() { 36 | const p = this.props; 37 | return _.map(new Array(p.items), function(item, i) { 38 | const w = _.random(1, 2); 39 | const h = _.random(1, 3); 40 | return { 41 | x: (i * 2) % 12, 42 | y: Math.floor(i / 6), 43 | w: w, 44 | h: h, 45 | i: i.toString() 46 | }; 47 | }); 48 | } 49 | 50 | onLayoutChange(layout) { 51 | this.props.onLayoutChange(layout); 52 | } 53 | 54 | onResize(layout, oldLayoutItem, layoutItem, placeholder) { 55 | // `oldLayoutItem` contains the state of the item before the resize. 56 | // You can modify `layoutItem` to enforce constraints. 57 | 58 | if (layoutItem.h < 3 && layoutItem.w > 2) { 59 | layoutItem.w = 2; 60 | placeholder.w = 2; 61 | } 62 | 63 | if (layoutItem.h >= 3 && layoutItem.w < 2) { 64 | layoutItem.w = 2; 65 | placeholder.w = 2; 66 | } 67 | } 68 | 69 | render() { 70 | return ( 71 | 76 | {this.generateDOM()} 77 | 78 | ); 79 | } 80 | } 81 | 82 | if (process.env.STATIC_EXAMPLES === true) { 83 | import("../test-hook.jsx").then(fn => fn.default(DynamicMinMaxLayout)); 84 | } 85 | -------------------------------------------------------------------------------- /test/examples/11-no-vertical-compact.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | export default class NoCompactingLayout extends React.PureComponent { 8 | static defaultProps = { 9 | className: "layout", 10 | items: 50, 11 | cols: 12, 12 | rowHeight: 30, 13 | onLayoutChange: function() {}, 14 | // This turns off compaction so you can place items wherever. 15 | verticalCompact: false 16 | }; 17 | 18 | constructor(props) { 19 | super(props); 20 | 21 | const layout = this.generateLayout(); 22 | this.state = { layout }; 23 | } 24 | 25 | generateDOM() { 26 | return _.map(_.range(this.props.items), function(i) { 27 | return ( 28 |
29 | {i} 30 |
31 | ); 32 | }); 33 | } 34 | 35 | generateLayout() { 36 | const p = this.props; 37 | return _.map(new Array(p.items), function(item, i) { 38 | const y = _.result(p, "y") || Math.ceil(Math.random() * 4) + 1; 39 | return { 40 | x: (i * 2) % 12, 41 | y: Math.floor(i / 6) * y, 42 | w: 2, 43 | h: y, 44 | i: i.toString() 45 | }; 46 | }); 47 | } 48 | 49 | onLayoutChange(layout) { 50 | this.props.onLayoutChange(layout); 51 | } 52 | 53 | render() { 54 | return ( 55 | 60 | {this.generateDOM()} 61 | 62 | ); 63 | } 64 | } 65 | 66 | if (process.env.STATIC_EXAMPLES === true) { 67 | import("../test-hook.jsx").then(fn => fn.default(NoCompactingLayout)); 68 | } 69 | -------------------------------------------------------------------------------- /test/examples/12-prevent-collision.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | export default class NoCollisionLayout extends React.PureComponent { 8 | static defaultProps = { 9 | className: "layout", 10 | items: 50, 11 | cols: 12, 12 | rowHeight: 30, 13 | onLayoutChange: function() {}, 14 | // This turns off compaction so you can place items wherever. 15 | verticalCompact: false, 16 | // This turns off rearrangement so items will not be pushed arround. 17 | preventCollision: true 18 | }; 19 | 20 | constructor(props) { 21 | super(props); 22 | 23 | const layout = this.generateLayout(); 24 | this.state = { layout }; 25 | } 26 | 27 | generateDOM() { 28 | return _.map(_.range(this.props.items), function(i) { 29 | return ( 30 |
31 | {i} 32 |
33 | ); 34 | }); 35 | } 36 | 37 | generateLayout() { 38 | const p = this.props; 39 | return _.map(new Array(p.items), function(item, i) { 40 | const y = _.result(p, "y") || Math.ceil(Math.random() * 4) + 1; 41 | return { 42 | x: (i * 2) % 12, 43 | y: Math.floor(i / 6) * y, 44 | w: 2, 45 | h: y, 46 | i: i.toString() 47 | }; 48 | }); 49 | } 50 | 51 | onLayoutChange(layout) { 52 | this.props.onLayoutChange(layout); 53 | } 54 | 55 | render() { 56 | return ( 57 | 62 | {this.generateDOM()} 63 | 64 | ); 65 | } 66 | } 67 | 68 | if (process.env.STATIC_EXAMPLES === true) { 69 | import("../test-hook.jsx").then(fn => fn.default(NoCollisionLayout)); 70 | } 71 | -------------------------------------------------------------------------------- /test/examples/13-error-case.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RGL, { WidthProvider } from "react-grid-layout"; 3 | 4 | const ReactGridLayout = WidthProvider(RGL); 5 | 6 | export default class ErrorCaseLayout extends React.PureComponent { 7 | static defaultProps = { 8 | className: "layout", 9 | items: 3, 10 | rowHeight: 100, 11 | onLayoutChange: function() {}, 12 | cols: 2 13 | }; 14 | 15 | constructor(props) { 16 | super(props); 17 | 18 | const layout = this.generateLayout(); 19 | this.state = { layout }; 20 | } 21 | 22 | generateDOM() { 23 | return [ 24 |
25 | {"1"} 26 |
, 27 |
28 | {"2"} 29 |
, 30 |
31 | {"3"} 32 |
33 | ]; 34 | } 35 | 36 | generateLayout() { 37 | return [ 38 | { 39 | x: 0, 40 | y: 0, 41 | w: 1, 42 | h: 1, 43 | i: "1" 44 | }, 45 | { 46 | x: 1, 47 | y: 0, 48 | w: 1, 49 | h: 1, 50 | i: "2" 51 | }, 52 | { 53 | x: 0, 54 | y: 1, 55 | w: 2, 56 | h: 2, 57 | i: "3" 58 | } 59 | ]; 60 | } 61 | 62 | onLayoutChange(layout) { 63 | this.props.onLayoutChange(layout); 64 | } 65 | 66 | render() { 67 | return ( 68 | 73 | {this.generateDOM()} 74 | 75 | ); 76 | } 77 | } 78 | 79 | if (process.env.STATIC_EXAMPLES === true) { 80 | import("../test-hook.jsx").then(fn => fn.default(ErrorCaseLayout)); 81 | } 82 | -------------------------------------------------------------------------------- /test/examples/14-toolbox.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import { Responsive, WidthProvider } from "react-grid-layout"; 4 | const ResponsiveReactGridLayout = WidthProvider(Responsive); 5 | 6 | class ToolBoxItem extends React.Component { 7 | render() { 8 | return ( 9 |
13 | {this.props.item.i} 14 |
15 | ); 16 | } 17 | } 18 | class ToolBox extends React.Component { 19 | render() { 20 | return ( 21 |
22 | Toolbox 23 |
24 | {this.props.items.map(item => ( 25 | 30 | ))} 31 |
32 |
33 | ); 34 | } 35 | } 36 | 37 | export default class ToolboxLayout extends React.Component { 38 | static defaultProps = { 39 | className: "layout", 40 | rowHeight: 30, 41 | onLayoutChange: function() {}, 42 | cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }, 43 | initialLayout: generateLayout() 44 | }; 45 | 46 | state = { 47 | currentBreakpoint: "lg", 48 | compactType: "vertical", 49 | mounted: false, 50 | layouts: { lg: this.props.initialLayout }, 51 | toolbox: { lg: [] } 52 | }; 53 | 54 | componentDidMount() { 55 | this.setState({ mounted: true }); 56 | } 57 | 58 | generateDOM() { 59 | return _.map(this.state.layouts[this.state.currentBreakpoint], l => { 60 | return ( 61 |
62 |
63 | × 64 |
65 | {l.static ? ( 66 | 70 | Static - {l.i} 71 | 72 | ) : ( 73 | {l.i} 74 | )} 75 |
76 | ); 77 | }); 78 | } 79 | 80 | onBreakpointChange = breakpoint => { 81 | this.setState(prevState => ({ 82 | currentBreakpoint: breakpoint, 83 | toolbox: { 84 | ...prevState.toolbox, 85 | [breakpoint]: 86 | prevState.toolbox[breakpoint] || 87 | prevState.toolbox[prevState.currentBreakpoint] || 88 | [] 89 | } 90 | })); 91 | }; 92 | 93 | onCompactTypeChange = () => { 94 | const { compactType: oldCompactType } = this.state; 95 | const compactType = 96 | oldCompactType === "horizontal" 97 | ? "vertical" 98 | : oldCompactType === "vertical" 99 | ? null 100 | : "horizontal"; 101 | this.setState({ compactType }); 102 | }; 103 | 104 | onTakeItem = item => { 105 | this.setState(prevState => ({ 106 | toolbox: { 107 | ...prevState.toolbox, 108 | [prevState.currentBreakpoint]: prevState.toolbox[ 109 | prevState.currentBreakpoint 110 | ].filter(({ i }) => i !== item.i) 111 | }, 112 | layouts: { 113 | ...prevState.layouts, 114 | [prevState.currentBreakpoint]: [ 115 | ...prevState.layouts[prevState.currentBreakpoint], 116 | item 117 | ] 118 | } 119 | })); 120 | }; 121 | 122 | onPutItem = item => { 123 | this.setState(prevState => { 124 | return { 125 | toolbox: { 126 | ...prevState.toolbox, 127 | [prevState.currentBreakpoint]: [ 128 | ...(prevState.toolbox[prevState.currentBreakpoint] || []), 129 | item 130 | ] 131 | }, 132 | layouts: { 133 | ...prevState.layouts, 134 | [prevState.currentBreakpoint]: prevState.layouts[ 135 | prevState.currentBreakpoint 136 | ].filter(({ i }) => i !== item.i) 137 | } 138 | }; 139 | }); 140 | }; 141 | 142 | onLayoutChange = (layout, layouts) => { 143 | this.props.onLayoutChange(layout, layouts); 144 | this.setState({ layouts }); 145 | }; 146 | 147 | onNewLayout = () => { 148 | this.setState({ 149 | layouts: { lg: generateLayout() } 150 | }); 151 | }; 152 | 153 | render() { 154 | return ( 155 |
156 |
157 | Current Breakpoint: {this.state.currentBreakpoint} ( 158 | {this.props.cols[this.state.currentBreakpoint]} columns) 159 |
160 |
161 | Compaction type:{" "} 162 | {_.capitalize(this.state.compactType) || "No Compaction"} 163 |
164 | 165 | 168 | 169 | 173 | 174 | 187 | {this.generateDOM()} 188 | 189 |
190 | ); 191 | } 192 | } 193 | 194 | function generateLayout() { 195 | return _.map(_.range(0, 25), function(item, i) { 196 | var y = Math.ceil(Math.random() * 4) + 1; 197 | return { 198 | x: (_.random(0, 5) * 2) % 12, 199 | y: Math.floor(i / 6) * y, 200 | w: 2, 201 | h: y, 202 | i: i.toString(), 203 | static: Math.random() < 0.05 204 | }; 205 | }); 206 | } 207 | 208 | if (process.env.STATIC_EXAMPLES === true) { 209 | import("../test-hook.jsx").then(fn => fn.default(ToolboxLayout)); 210 | } 211 | -------------------------------------------------------------------------------- /test/examples/15-drag-from-outside.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import { Responsive, WidthProvider } from "react-grid-layout"; 4 | const ResponsiveReactGridLayout = WidthProvider(Responsive); 5 | 6 | export default class DragFromOutsideLayout extends React.Component { 7 | static defaultProps = { 8 | className: "layout", 9 | rowHeight: 30, 10 | onLayoutChange: function() {}, 11 | cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }, 12 | }; 13 | 14 | state = { 15 | currentBreakpoint: "lg", 16 | compactType: "vertical", 17 | mounted: false, 18 | layouts: { lg: generateLayout() } 19 | }; 20 | 21 | componentDidMount() { 22 | this.setState({ mounted: true }); 23 | } 24 | 25 | generateDOM() { 26 | return _.map(this.state.layouts.lg, function(l, i) { 27 | return ( 28 |
29 | {l.static ? ( 30 | 34 | Static - {i} 35 | 36 | ) : ( 37 | {i} 38 | )} 39 |
40 | ); 41 | }); 42 | } 43 | 44 | onBreakpointChange = breakpoint => { 45 | this.setState({ 46 | currentBreakpoint: breakpoint 47 | }); 48 | }; 49 | 50 | onCompactTypeChange = () => { 51 | const { compactType: oldCompactType } = this.state; 52 | const compactType = 53 | oldCompactType === "horizontal" 54 | ? "vertical" 55 | : oldCompactType === "vertical" 56 | ? null 57 | : "horizontal"; 58 | this.setState({ compactType }); 59 | }; 60 | 61 | onLayoutChange = (layout, layouts) => { 62 | this.props.onLayoutChange(layout, layouts); 63 | }; 64 | 65 | onNewLayout = () => { 66 | this.setState({ 67 | layouts: { lg: generateLayout() } 68 | }); 69 | }; 70 | 71 | onDrop = (layout, layoutItem, _event) => { 72 | alert(`Dropped element props:\n${JSON.stringify(layoutItem, ['x', 'y', 'w', 'h'], 2)}`); 73 | }; 74 | 75 | render() { 76 | return ( 77 |
78 |
79 | Current Breakpoint: {this.state.currentBreakpoint} ( 80 | {this.props.cols[this.state.currentBreakpoint]} columns) 81 |
82 |
83 | Compaction type:{" "} 84 | {_.capitalize(this.state.compactType) || "No Compaction"} 85 |
86 | 87 | 90 |
e.dataTransfer.setData("text/plain", "")} 99 | > 100 | Droppable Element (Drag me!) 101 |
102 | 117 | {this.generateDOM()} 118 | 119 |
120 | ); 121 | } 122 | } 123 | 124 | function generateLayout() { 125 | return _.map(_.range(0, 25), function(item, i) { 126 | var y = Math.ceil(Math.random() * 4) + 1; 127 | return { 128 | x: Math.round(Math.random() * 5) * 2, 129 | y: Math.floor(i / 6) * y, 130 | w: 2, 131 | h: y, 132 | i: i.toString(), 133 | static: Math.random() < 0.05 134 | }; 135 | }); 136 | } 137 | 138 | if (process.env.STATIC_EXAMPLES === true) { 139 | import("../test-hook.jsx").then(fn => fn.default(DragFromOutsideLayout)); 140 | } 141 | -------------------------------------------------------------------------------- /test/examples/16-bounded.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | class BoundedLayout extends React.PureComponent { 8 | static defaultProps = { 9 | className: "layout", 10 | items: 20, 11 | rowHeight: 30, 12 | onLayoutChange: function() {}, 13 | cols: 12 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | const layout = this.generateLayout(); 20 | this.state = { layout }; 21 | } 22 | 23 | generateDOM() { 24 | return _.map(_.range(this.props.items), function(i) { 25 | return ( 26 |
27 | {i} 28 |
29 | ); 30 | }); 31 | } 32 | 33 | generateLayout() { 34 | const p = this.props; 35 | return _.map(new Array(p.items), function(item, i) { 36 | const y = _.result(p, "y") || Math.ceil(Math.random() * 4) + 1; 37 | return { 38 | x: (i * 2) % 12, 39 | y: Math.floor(i / 6) * y, 40 | w: 2, 41 | h: y, 42 | i: i.toString() 43 | }; 44 | }); 45 | } 46 | 47 | onLayoutChange(layout) { 48 | this.props.onLayoutChange(layout); 49 | } 50 | 51 | render() { 52 | return ( 53 | 59 | {this.generateDOM()} 60 | 61 | ); 62 | } 63 | } 64 | 65 | if (process.env.STATIC_EXAMPLES === true) { 66 | import("../test-hook.jsx").then(fn => fn.default(BoundedLayout)); 67 | } 68 | -------------------------------------------------------------------------------- /test/examples/17-responsive-bootstrap-style.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { WidthProvider, Responsive } from "react-grid-layout"; 3 | 4 | const ResponsiveReactGridLayout = WidthProvider(Responsive); 5 | 6 | /** 7 | * This example illustrates how to let grid items lay themselves out with a bootstrap-style specification. 8 | */ 9 | export default class BootstrapStyleLayout extends React.PureComponent { 10 | static defaultProps = { 11 | isDraggable: true, 12 | isResizable: true, 13 | items: 20, 14 | rowHeight: 30, 15 | onLayoutChange: function() {}, 16 | cols: {lg: 12, md: 12, sm: 12, xs: 12, xxs: 12} 17 | }; 18 | 19 | state = { 20 | layouts: this.generateLayouts() 21 | }; 22 | 23 | onLayoutChange(layout) { 24 | this.props.onLayoutChange(layout); 25 | } 26 | 27 | generateDOM() { 28 | return [...Array(this.props.items)].map((_, i) => ( 29 |
30 | {i} 31 |
32 | )); 33 | } 34 | 35 | // Create responsive layouts. Similar to bootstrap, increase the pseudo width as 36 | // the viewport shrinks 37 | generateLayouts() { 38 | const times = [...Array(this.props.items)]; 39 | const widths = {lg: 3, md: 4, sm: 6, xs: 12, xxs: 12}; 40 | return Object.keys(widths).reduce((memo, breakpoint) => { 41 | const width = widths[breakpoint]; 42 | const cols = this.props.cols[breakpoint]; 43 | memo[breakpoint] = [ 44 | // You can set y to 0, the collision algo will figure it out. 45 | ...times.map((_, i) => ({x: (i * width) % cols, y: 0, w: width, h: 4, i: String(i)})) 46 | ]; 47 | return memo; 48 | }, {}); 49 | } 50 | 51 | render() { 52 | return ( 53 | 58 | {this.generateDOM()} 59 | 60 | ); 61 | } 62 | } 63 | 64 | if (process.env.STATIC_EXAMPLES === true) { 65 | import("../test-hook.jsx").then(fn => fn.default(BootstrapStyleLayout)); 66 | } 67 | -------------------------------------------------------------------------------- /test/examples/18-scale.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | export default class ScaledLayout extends React.PureComponent { 8 | static defaultProps = { 9 | className: "layout", 10 | items: 20, 11 | rowHeight: 30, 12 | onLayoutChange: function() {}, 13 | cols: 12, 14 | transformScale: 0.5 15 | }; 16 | 17 | constructor(props) { 18 | super(props); 19 | 20 | const layout = this.generateLayout(); 21 | this.state = { layout }; 22 | } 23 | 24 | generateDOM() { 25 | return _.map(_.range(this.props.items), function(i) { 26 | return ( 27 |
28 | {i} 29 |
30 | ); 31 | }); 32 | } 33 | 34 | generateLayout() { 35 | const p = this.props; 36 | return _.map(new Array(p.items), function(item, i) { 37 | const y = _.result(p, "y") || Math.ceil(Math.random() * 4) + 1; 38 | return { 39 | x: (i * 2) % 12, 40 | y: Math.floor(i / 6) * y, 41 | w: 2, 42 | h: y, 43 | i: i.toString() 44 | }; 45 | }); 46 | } 47 | 48 | onLayoutChange(layout) { 49 | this.props.onLayoutChange(layout); 50 | } 51 | 52 | render() { 53 | return ( 54 |
55 | 60 | {this.generateDOM()} 61 | 62 |
63 | ); 64 | } 65 | } 66 | 67 | if (process.env.STATIC_EXAMPLES === true) { 68 | import("../test-hook.jsx").then(fn => fn.default(ScaledLayout)); 69 | } 70 | -------------------------------------------------------------------------------- /test/examples/19-allow-overlap.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | export default class AllowOverlap extends React.PureComponent { 8 | static defaultProps = { 9 | className: "layout", 10 | items: 20, 11 | rowHeight: 30, 12 | onLayoutChange: function() {}, 13 | cols: 12 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | const layout = this.generateLayout(); 20 | this.state = { layout }; 21 | } 22 | 23 | generateDOM() { 24 | return _.map(_.range(this.props.items), function(i) { 25 | return ( 26 |
27 | {i} 28 |
29 | ); 30 | }); 31 | } 32 | 33 | generateLayout() { 34 | const p = this.props; 35 | return _.map(new Array(p.items), function(item, i) { 36 | const y = _.result(p, "y") || Math.ceil(Math.random() * 4) + 1; 37 | return { 38 | x: (i * 2) % 12, 39 | y: Math.floor(i / 6) * y, 40 | w: 2, 41 | h: y, 42 | i: i.toString() 43 | }; 44 | }); 45 | } 46 | 47 | onLayoutChange(layout) { 48 | this.props.onLayoutChange(layout); 49 | } 50 | 51 | render() { 52 | return ( 53 | 60 | {this.generateDOM()} 61 | 62 | ); 63 | } 64 | } 65 | 66 | if (process.env.STATIC_EXAMPLES === true) { 67 | import("../test-hook.jsx").then(fn => fn.default(AllowOverlap)); 68 | } 69 | -------------------------------------------------------------------------------- /test/examples/20-resizable-handles.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | export default class ResizableHandles extends React.PureComponent { 8 | static defaultProps = { 9 | className: "layout", 10 | items: 20, 11 | rowHeight: 30, 12 | onLayoutChange: function() {}, 13 | cols: 12 14 | }; 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | const layout = this.generateLayout(); 20 | this.state = { layout }; 21 | } 22 | 23 | generateDOM() { 24 | return _.map(_.range(this.props.items), function(i) { 25 | return ( 26 |
27 | {i} 28 |
29 | ); 30 | }); 31 | } 32 | 33 | generateLayout() { 34 | const p = this.props; 35 | const availableHandles = ["s", "w", "e", "n", "sw", "nw", "se", "ne"]; 36 | 37 | return _.map(new Array(p.items), function(item, i) { 38 | const y = _.result(p, "y") || Math.ceil(Math.random() * 4) + 1; 39 | return { 40 | x: (i * 2) % 12, 41 | y: Math.floor(i / 6) * y, 42 | w: 2, 43 | h: y, 44 | i: i.toString(), 45 | resizeHandles: availableHandles 46 | }; 47 | }); 48 | } 49 | 50 | onLayoutChange(layout) { 51 | this.props.onLayoutChange(layout); 52 | } 53 | 54 | render() { 55 | return ( 56 | 61 | {this.generateDOM()} 62 | 63 | ); 64 | } 65 | } 66 | 67 | if (process.env.STATIC_EXAMPLES === true) { 68 | import("../test-hook.jsx").then(fn => fn.default(ResizableHandles)); 69 | } 70 | -------------------------------------------------------------------------------- /test/examples/21-horizontal.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import _ from "lodash"; 3 | import RGL, { WidthProvider } from "react-grid-layout"; 4 | 5 | const ReactGridLayout = WidthProvider(RGL); 6 | 7 | export default class Horizontal extends React.PureComponent { 8 | static defaultProps = { 9 | className: "layout", 10 | items: 5, 11 | rowHeight: 5, 12 | onLayoutChange: function() {}, 13 | cols: 12, 14 | compactType: "horizontal", 15 | maxRows: 1, 16 | allowOverlap: false 17 | }; 18 | 19 | constructor(props) { 20 | super(props); 21 | 22 | const layout = this.generateLayout(); 23 | this.state = { layout }; 24 | } 25 | 26 | generateDOM() { 27 | return _.map(_.range(this.props.items), function(i) { 28 | return ( 29 |
30 | {i} 31 |
32 | ); 33 | }); 34 | } 35 | 36 | generateLayout() { 37 | const p = this.props; 38 | return _.map(new Array(p.items), function(item, i) { 39 | const y = 5; 40 | return { 41 | x: (i * 2) % 12, 42 | y: Math.floor(i / 6) * y, 43 | w: 2, 44 | h: y, 45 | i: i.toString() 46 | }; 47 | }); 48 | } 49 | 50 | onLayoutChange(layout) { 51 | this.props.onLayoutChange(layout); 52 | } 53 | 54 | render() { 55 | return ( 56 | 63 | {this.generateDOM()} 64 | 65 | ); 66 | } 67 | } 68 | 69 | if (process.env.STATIC_EXAMPLES === true) { 70 | import("../test-hook.jsx").then(fn => fn.default(Horizontal)); 71 | } 72 | -------------------------------------------------------------------------------- /test/test-hook.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "style-loader!css-loader!../css/styles.css"; 4 | import "style-loader!css-loader!../examples/util/example-styles.css"; 5 | typeof window !== "undefined" && (window.React = React); // for devtools 6 | 7 | export default function makeLayout(Layout) { 8 | // Basic layout that mirrors the internals of its child layout by listening to `onLayoutChange`. 9 | // It does not pass any other props to the Layout. 10 | class ListeningLayout extends React.Component { 11 | state = { layout: [] }; 12 | 13 | onLayoutChange = layout => { 14 | this.setState({ layout: layout }); 15 | }; 16 | 17 | stringifyLayout() { 18 | return this.state.layout.map(function (l) { 19 | const name = l.i === "__dropping-elem__" ? "drop" : l.i; 20 | return ( 21 |
22 | {name} 23 | {`: [${l.x}, ${l.y}, ${l.w}, ${l.h}]`} 24 |
25 | ); 26 | }); 27 | } 28 | 29 | render() { 30 | return ( 31 | 32 |
33 |
34 | Displayed as [x, y, w, h]: 35 |
{this.stringifyLayout()}
36 |
37 | 38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | function run() { 45 | const contentDiv = document.getElementById("content"); 46 | const gridProps = window.gridProps || {}; 47 | ReactDOM.render( 48 | React.createElement(ListeningLayout, gridProps), 49 | contentDiv 50 | ); 51 | } 52 | if (!document.getElementById("content")) { 53 | document.addEventListener("DOMContentLoaded", run); 54 | } else { 55 | run(); 56 | } 57 | 58 | return ListeningLayout; 59 | } 60 | -------------------------------------------------------------------------------- /test/util/deepFreeze.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // Deep freeze an object by using a Proxy. 4 | // This is better than Object.freeze() as we can create coherent error messages 5 | // and easily only deliver frozen subobjects when they are accessed. We can 6 | // even add custom logic, like warning if you access a property that doesn't exist. 7 | /* eslint-disable no-console */ 8 | export default function deepFreeze( 9 | inputObj: T, 10 | options: { get: boolean, set: boolean } = { get: true, set: true } 11 | ): $ReadOnly<{| ...T |}> { 12 | // Our handler that rejects any change to the object and any nested objects inside it 13 | const deepFreezer = {}; 14 | if (options.get) { 15 | deepFreezer.get = function get( 16 | obj: T, 17 | prop: $Keys, 18 | _receiver: Proxy 19 | ): any { 20 | // Clone w/o Proxy 21 | if (prop === "toJSON") return () => obj; 22 | // If dealing with nested object, nest the proxy untill it reaches the direct property of it's parent proxy 23 | if (typeof obj[prop] === "object" && obj[prop] !== null) { 24 | return new Proxy(obj[prop], deepFreezer); 25 | } 26 | // If prop is directly accessible, just do the default operation 27 | else { 28 | if ( 29 | !(prop in obj) && 30 | prop !== "length" && 31 | prop !== "__esModule" && 32 | typeof prop !== "symbol" 33 | ) { 34 | throw new Error( 35 | `Can not get unknown prop "${String(prop)}" on frozen object.` 36 | ); 37 | } 38 | return obj[prop]; 39 | } 40 | }; 41 | } 42 | if (options.set) { 43 | deepFreezer.set = function set( 44 | obj: T, 45 | prop: string, 46 | _val: any, 47 | _rec: Proxy 48 | ): boolean { 49 | throw new Error(`Can not set unknown prop "${prop}" on frozen object.`); 50 | }; 51 | } 52 | // $FlowIgnore for all useful purposes the output type is T here 53 | return new Proxy(inputObj, deepFreezer); 54 | } 55 | -------------------------------------------------------------------------------- /test/util/setupTests.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Enzyme from "enzyme"; 4 | import Adapter from "enzyme-adapter-react-16"; 5 | 6 | Enzyme.configure({ adapter: new Adapter() }); 7 | 8 | // We rely on sort() being deterministic for tests, but it changed from QuickSort to TimSort 9 | // in Node 12. This breaks tests, so we monkey-patch it. 10 | import { sort } from "timsort"; 11 | 12 | // $FlowIgnore dirty hack 13 | Array.prototype.sort = function (comparator) { 14 | sort(this, comparator); 15 | return this; 16 | }; 17 | 18 | // Required in drag code, not working in JSDOM 19 | Object.defineProperty(HTMLElement.prototype, "offsetParent", { 20 | get() { 21 | // $FlowIgnore[object-this-reference] 22 | return this.parentNode; 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /webpack-dev-server.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const path = require("path"); 3 | const webpack = require("webpack"); 4 | 5 | module.exports = { 6 | mode: "development", 7 | context: __dirname, 8 | entry: "./test/dev-hook.jsx", 9 | output: { 10 | path: "/", 11 | filename: "bundle.js", 12 | sourceMapFilename: "[file].map", 13 | publicPath: "/" 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.jsx?$/, 19 | exclude: /node_modules/, 20 | loader: "babel-loader", 21 | options: { 22 | cacheDirectory: true, 23 | plugins: [["react-hot-loader/babel"]] 24 | } 25 | } 26 | ] 27 | }, 28 | plugins: [ 29 | new webpack.DefinePlugin({ 30 | "process.env": { 31 | NODE_ENV: JSON.stringify("development") 32 | } 33 | }) 34 | ], 35 | devtool: "eval", 36 | devServer: { 37 | compress: true, 38 | port: 4002, 39 | open: "index-dev.html", 40 | client: { 41 | overlay: true 42 | }, 43 | static: { 44 | directory: "." 45 | } 46 | }, 47 | resolve: { 48 | extensions: [".webpack.js", ".web.js", ".js", ".jsx"], 49 | alias: { 50 | "react-grid-layout": path.join(__dirname, "/index-dev.js") 51 | } 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /webpack-examples.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const webpack = require("webpack"); 3 | const fs = require("fs"); 4 | 5 | // Builds example bundles 6 | module.exports = { 7 | mode: "development", 8 | context: __dirname, 9 | entry: {}, 10 | optimization: { 11 | splitChunks: { 12 | cacheGroups: { 13 | commons: { 14 | name: "commons", 15 | chunks: "all", 16 | minChunks: 1 17 | } 18 | } 19 | } 20 | }, 21 | output: { 22 | path: __dirname + "/examples", 23 | filename: "[name].js", 24 | sourceMapFilename: "[file].map", 25 | publicPath: "auto" 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.jsx?$/, 31 | exclude: /node_modules/, 32 | loader: "babel-loader", 33 | options: { 34 | cacheDirectory: true 35 | } 36 | } 37 | ] 38 | }, 39 | plugins: [ 40 | new webpack.DefinePlugin({ 41 | "process.env.STATIC_EXAMPLES": JSON.stringify(true) 42 | }) 43 | ], 44 | devServer: { 45 | compress: true, 46 | port: 4002, 47 | open: "/react-grid-layout/examples/00-showcase.html", 48 | client: { 49 | overlay: true 50 | }, 51 | static: { 52 | directory: ".", 53 | publicPath: "/react-grid-layout" 54 | } 55 | }, 56 | resolve: { 57 | extensions: [".js", ".jsx"], 58 | alias: { "react-grid-layout": __dirname + "/index-dev.js" } 59 | } 60 | }; 61 | 62 | // Load all entry points 63 | const files = fs 64 | .readdirSync(__dirname + "/test/examples") 65 | .filter(element => element.match(/^.+\.jsx$/)); 66 | 67 | for (const file of files) { 68 | const module_name = file.replace(/\.jsx$/, ""); 69 | module.exports.entry[module_name] = "./test/examples/" + file; 70 | } 71 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | const webpack = require("webpack"); 3 | 4 | // Builds bundle usable