Repository: AirLabsTeam/react-drag-to-select
Branch: main
Commit: 0b93b3a4c648
Files: 40
Total size: 43.0 KB
Directory structure:
gitextract_jvshjrpn/
├── .eslintignore
├── .eslintrc.cjs
├── .github/
│ └── workflows/
│ ├── e2e-tests.yml
│ ├── publish.yml
│ └── unit-tests.yml
├── .gitignore
├── .nvmrc
├── .prettierrc
├── LICENSE
├── README.md
├── cypress/
│ ├── e2e/
│ │ └── selecting.cy.js
│ └── support/
│ ├── commands.ts
│ └── e2e.ts
├── cypress.config.ts
├── example/
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public/
│ │ ├── index.html
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src/
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── index.css
│ │ ├── index.tsx
│ │ ├── react-app-env.d.ts
│ │ ├── reportWebVitals.ts
│ │ └── setupTests.ts
│ └── tsconfig.json
├── jest.config.cjs
├── package.json
├── src/
│ ├── components/
│ │ └── SelectionContainer.tsx
│ ├── hooks/
│ │ ├── useSelectionContainer.tsx
│ │ └── useSelectionLogic.ts
│ ├── index.ts
│ ├── typings.d.ts
│ └── utils/
│ ├── __tests__/
│ │ └── boxes.test.ts
│ ├── boxes.ts
│ └── types.ts
├── tsconfig.eslint.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
build/
dist/
node_modules/
.snapshots/
*.min.js
cypress
cypress.config.ts
example/
coverage/
================================================
FILE: .eslintrc.cjs
================================================
module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'prettier'],
root: true,
rules: {
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'prettier/prettier': 'error',
'@typescript-eslint/strict-boolean-expressions': 'error',
},
overrides: [
{
files: ['*.ts', '*.tsx'],
parserOptions: {
project: 'tsconfig.eslint.json',
sourceType: 'module',
},
},
],
};
================================================
FILE: .github/workflows/e2e-tests.yml
================================================
name: End-to-end tests
on: [push]
jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
- name: Cache root dependencies
id: root-cache
uses: actions/cache@v3
with:
path: ./node_modules
key: modules-${{ hashFiles('package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --ignore-scripts
- name: Cache example dependencies
id: example-cache
uses: actions/cache@v3
with:
path: example/node_modules
key: modules-${{ hashFiles('example/package-lock.json') }}
- name: Install example dependencies
if: steps.example-cache.outputs.cache-hit != 'true'
run: cd example; npm ci --ignore-scripts
- name: Build project
run: npm run build
- name: Cypress run
uses: cypress-io/github-action@v4
with:
browser: chrome
start: npm run ci:start-example
wait-on: 'http://localhost:3000'
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish to npmjs
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: main
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org'
scope: '@air'
- name: Configure Git
run: |
git config user.email "dev@air.inc"
git config user.name "air-dev-bo"
- run: npm version ${{ github.event.release.tag_name }} -m "Release ${{ github.event.release.tag_name }} 📣"
- name: Push version to main
uses: CasperWA/push-protected@v2
with:
token: ${{ secrets.AIR_DEV_BOT_PAT }}
branch: main
unprotect_reviews: true
- name: Cache root dependencies
id: root-cache
uses: actions/cache@v3
with:
path: ./node_modules
key: modules-${{ hashFiles('package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --ignore-scripts
- run: npm run build
- run: npm ci
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .github/workflows/unit-tests.yml
================================================
name: Unit tests
on: [push]
jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
- name: Cache root dependencies
id: root-cache
uses: actions/cache@v3
with:
path: ./node_modules
key: modules-${{ hashFiles('package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci --ignore-scripts
- name: Run unit tests
run: npm run test
================================================
FILE: .gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
node_modules
# builds
build
dist
.rpt2_cache
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea
# package testing
.yalc
# tests
coverage
================================================
FILE: .nvmrc
================================================
22.16.0
================================================
FILE: .prettierrc
================================================
{
"arrowParens": "always",
"printWidth": 120,
"semi": true,
"singleQuote": true,
"trailingComma": "all"
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Air
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<p align="center">
<img style="width: 800px" src="https://user-images.githubusercontent.com/1065539/184370954-6398b161-25f5-455c-9dcb-197588b40057.gif" />
</p>
<h1 align="center">React drag-to-select</h1>
<p align="center"><i>A highly-performant React library which adds drag-to-select to your app.</i></p>
<p align="center">
<a href="https://www.npmjs.com/package/@air/react-drag-to-select">
<img src="https://img.shields.io/npm/v/@air/react-drag-to-select?color=2E77FF" alt="size" />
</a>
<img alt="e2e" src="https://github.com/AirLabsTeam/react-drag-to-select/actions/workflows/e2e-tests.yml/badge.svg" />
<img alt="unit" src="https://github.com/AirLabsTeam/react-drag-to-select/actions/workflows/unit-tests.yml/badge.svg" />
<img alt="size" src="https://img.shields.io/bundlephobia/min/@air/react-drag-to-select" />
</p>
## ✨ Features <a name="features"></a>
- Near 60 fps in 6x CPU slowdown on 2.3 GHz Quad-Core Intel Core i7
- Simple API. It doesn't actually select items; just draws the selection box and passes you coordinates so you can determine that (we provided a utility to help though)
- Fully built in TypeScript
- Unit and e2e tested
- Actively battle-tested in a [production-scale application](https://air.inc)
## Install
```bash
npm install --save @air/react-drag-to-select
```
```bash
yarn add @air/react-drag-to-select
```
## Usage
```tsx
import { useSelectionContainer } from '@air/react-drag-to-select'
const App = () => {
const { DragSelection } = useSelectionContainer();
return (
<div>
<DragSelection/>
<div>Selectable element</div>
</div>
)
}
```
Check out this codesandbox for a complete working example: https://codesandbox.io/s/billowing-lake-rzhid4
## useSelectionContainer arguments
|Name|Required|Type|Default|Description|
|----|--------|----|-------|-----------|
|`onSelectionStart`|No|`() => void`||Method called when selection starts (mouse is down and moved)|
|`onSelectionEnd`|No|`() => void`||Method called when selection ends (mouse is up)
|`onSelectionChange`|Yes|`(box: Box) => void`||Method called when selection moves|
|`isEnabled`|No|`boolean`|`true`|If false, selection does not fire|
|`eventsElement`|No|`Window`, `HTMLElement` or `null`|`window`|Element to listen mouse events|
|`selectionProps`|No|`React.HTMLAttributes`||Props of selection - you can pass style here as shown below|
|`shouldStartSelecting`|No|`() => boolean`|`undefined`|If supplied, this callback is fired on mousedown and can be used to prevent selection from starting. This is useful when you want to prevent certain areas of your application from being able to be selected. Returning true will enable selection and returning false will prevent selection from starting.|
## Selection styling
To style the selection box, pass `selectionProps: { style }` prop:
```tsx
const { DragSelection } = useSelectionContainer({
...,
selectionProps: {
style: {
border: '2px dashed purple',
borderRadius: 4,
backgroundColor: 'brown',
opacity: 0.5,
},
},
});
```
The default style for the selection box is
```ts
{
border: '1px solid #4C85D8',
background: 'rgba(155, 193, 239, 0.4)',
position: `absolute`,
zIndex: 99,
}
```
## Disabling selecting in certain areas
<p align="center">
<img style='width: 400px' src="example/assets/disable-select-example.gif">
</p>
Sometimes you want to disable a user being able to start selecting in a certain area. You can use the `shouldStartSelecting` prop for this.
```tsx
const { DragSelection } = useSelectionContainer({
shouldStartSelecting: (target) => {
/**
* In this example, we're preventing users from selecting in elements
* that have a data-disableselect attribute on them or one of their parents
*/
if (target instanceof HTMLElement) {
let el = target;
while (el.parentElement && !el.dataset.disableselect) {
el = el.parentElement;
}
return el.dataset.disableselect !== "true";
}
/**
* If the target doesn't exist, return false
* This would most likely not happen. It's really a TS safety check
*/
return false;
}
});
```
See full example here: https://codesandbox.io/s/exciting-rubin-xxf6r0
## Scrolling
Because we use the mouse position to calculate the selection box's coordinates, if your `<DragSelection />` is inside of an area that scrolls, you'll need to make some adjustments on your end. Our library can't inherently know which parent is being scrolled nor of it's position inside of the scrolling parent (if there are other sibling elements above it).
How this is solved on your end is modifiying the `left` (for horizontal scrolling) and `top` (for vertical scrolling) of the `selectionBox` that is passed to `handleSelectionChange`. See the [`onSelectionChange` in the example](https://github.com/AirLabsTeam/react-drag-to-select/blob/main/example/src/App.tsx#L20) for an idea of how to do this.
<img src="https://img.shields.io/npm/l/@air/react-drag-to-select?color=41C300" alt="MIT License" />
================================================
FILE: cypress/e2e/selecting.cy.js
================================================
/// <reference types="cypress" />
describe('react-drag-to-select example', () => {
beforeEach(() => {
cy.visit('/');
});
it('can select some items', () => {
cy.get('.container')
.trigger('mousedown', 10, 10, {
eventConstructor: 'MouseEvent',
})
.trigger('mousemove', 400, 150, {
eventConstructor: 'MouseEvent',
})
.trigger('mouseup');
for (let index = 0; index < 16; index++) {
if (index < 3) {
cy.get(`.element[data-testid="grid-cell-${index}"]`).should('have.class', 'selected');
} else {
cy.get(`.element[data-testid="grid-cell-${index}"]`).should('not.have.class', 'selected');
}
}
});
it('can select some items after scrolling', { scrollBehavior: false }, () => {
cy.viewport(500, 200);
cy.get('.element[data-testid="grid-cell-8"]').scrollIntoView()
cy.get('.container', { force: true })
.trigger('mousedown', 10, 320, {
eventConstructor: 'MouseEvent',
force: true
})
.trigger('mousemove', 320, 325, {
eventConstructor: 'MouseEvent',
force: true
})
.trigger('mouseup')
for (let index = 0; index < 16; index++) {
if (index > 7 && index < 11) {
cy.get(`.element[data-testid="grid-cell-${index}"]`).should('have.class', 'selected');
} else {
cy.get(`.element[data-testid="grid-cell-${index}"]`).should('not.have.class', 'selected');
}
}
});
});
================================================
FILE: cypress/support/commands.ts
================================================
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
================================================
FILE: cypress/support/e2e.ts
================================================
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
================================================
FILE: cypress.config.ts
================================================
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
setupNodeEvents(on, config) {
// implement node event listeners here
}
}
});
================================================
FILE: example/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: example/README.md
================================================
This example was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
It is linked to the react-drag-to-select package in the parent directory for development purposes.
You can run `yarn install` and then `yarn start` to test your package.
================================================
FILE: example/package.json
================================================
{
"name": "example",
"version": "0.1.0",
"private": true,
"dependencies": {
"@air/react-drag-to-select": "file:..",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.11.45",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"react": "file:../node_modules/react",
"react-dom": "file:../node_modules/react-dom",
"react-style-object-to-css": "file:../node_modules/react-style-object-to-css",
"typescript": "^4.7.4",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
================================================
FILE: example/public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
================================================
FILE: example/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: example/public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: example/src/App.css
================================================
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
================================================
FILE: example/src/App.tsx
================================================
import { useEffect, useRef, useState } from 'react';
import './App.css';
import { Box, boxesIntersect, useSelectionContainer } from '@air/react-drag-to-select';
function App() {
const [selectionBox, setSelectionBox] = useState<Box>();
const [selectedIndexes, setSelectedIndexes] = useState<number[]>([]);
const selectableItems = useRef<Box[]>([]);
const elementsContainerRef = useRef<HTMLDivElement | null>(null);
const { DragSelection } = useSelectionContainer({
eventsElement: document.getElementById('root'),
onSelectionChange: (box) => {
/**
* Here we make sure to adjust the box's left and top with the scroll position of the window
* @see https://github.com/AirLabsTeam/react-drag-to-select/#scrolling
*/
const scrollAwareBox = {
...box,
top: box.top + window.scrollY,
left: box.left + window.scrollX,
};
setSelectionBox(scrollAwareBox);
const indexesToSelect: number[] = [];
selectableItems.current.forEach((item, index) => {
if (boxesIntersect(scrollAwareBox, item)) {
indexesToSelect.push(index);
}
});
setSelectedIndexes(indexesToSelect);
},
onSelectionStart: () => {},
onSelectionEnd: () => {},
selectionProps: {
style: {
border: '2px dashed purple',
borderRadius: 4,
backgroundColor: 'brown',
opacity: 0.5,
},
},
shouldStartSelecting: (target) => {
// do something with target to determine if the user should start selecting
return true;
},
});
useEffect(() => {
if (elementsContainerRef.current) {
Array.from(elementsContainerRef.current.children).forEach((item) => {
const { left, top, width, height } = item.getBoundingClientRect();
selectableItems.current.push({
left,
top,
width,
height,
});
});
}
}, []);
return (
<div className="container">
<DragSelection />
<div id="elements-container" className="elements-container" ref={elementsContainerRef}>
{Array.from({ length: 16 }, (_, i) => (
<div
data-testid={`grid-cell-${i}`}
key={i}
className={`element ${selectedIndexes.includes(i) ? 'selected' : ''} `}
/>
))}
</div>
<div className="selection-box-info">
Selection Box:
<div>top: {selectionBox?.top || ''}</div>
<div>left: {selectionBox?.left || ''}</div>
<div>width: {selectionBox?.width || ''}</div>
<div>height: {selectionBox?.height || ''}</div>
</div>
</div>
);
}
export default App;
================================================
FILE: example/src/index.css
================================================
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
#root {
width: 100%;
height: 100vh;
}
.container {
padding: 50px;
}
.elements-container {
display: grid;
grid-template-columns: 100px 100px 100px 100px;
gap: 20px 20px;
}
.element {
width: 100px;
height: 100px;
border: 1px solid black;
}
.selected {
background-color: chocolate;
}
.selection-box-info {
margin-top: 10px;
}
================================================
FILE: example/src/index.tsx
================================================
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
================================================
FILE: example/src/react-app-env.d.ts
================================================
/// <reference types="react-scripts" />
================================================
FILE: example/src/reportWebVitals.ts
================================================
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
================================================
FILE: example/src/setupTests.ts
================================================
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
================================================
FILE: example/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
================================================
FILE: jest.config.cjs
================================================
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['node_modules', 'dist'],
collectCoverage: true
};
================================================
FILE: package.json
================================================
{
"name": "@air/react-drag-to-select",
"version": "5.0.11",
"description": "A performant React library which adds drag to select to your app",
"type": "module",
"author": "Air Labs, Inc.",
"license": "MIT",
"repository": "AirLabsTeam/react-drag-to-select",
"source": "src/index.ts",
"exports": {
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts",
"default": "./dist/index.modern.js"
},
"main": "./dist/index.cjs",
"module": "./dist/index.module.js",
"unpkg": "./dist/index.umd.js",
"typings": "dist/index",
"scripts": {
"lint": "npx eslint .",
"build": "npm run lint && microbundle",
"dev": "microbundle watch",
"pretty": "prettier --config .prettierrc 'src/**/*.(ts|tsx)' --write",
"test": "jest",
"ci:start-example": "cd example; npm start",
"cypress": "npx cypress open"
},
"dependencies": {
"react-style-object-to-css": "^1.1.2"
},
"peerDependencies": {
"react": "16 - 19",
"react-dom": "16 - 19"
},
"devDependencies": {
"@types/jest": "^28.1.6",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"cypress": "13.13.2",
"eslint": "^8.20.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^28.1.3",
"microbundle": "^0.15.0",
"prettier": "^2.7.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"ts-jest": "^28.0.7",
"typescript": "^4.7.4"
},
"files": [
"dist"
]
}
================================================
FILE: src/components/SelectionContainer.tsx
================================================
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { SelectionContainerRef, SelectionBox } from '../utils/types';
// @ts-ignore
import styleObjectToCSS from 'react-style-object-to-css';
export interface SelectionContainerProps extends React.HTMLAttributes<HTMLDivElement> {}
/**
* This is a component responsible for displaying mouse selection box
*/
export const SelectionContainer = forwardRef(({ style = {}, ...props }: SelectionContainerProps, ref) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const selectionBoxRef = useRef<HTMLDivElement>(null);
const [, setForceUpdate] = useState(0);
useImperativeHandle(
ref,
(): SelectionContainerRef => ({
getBoundingClientRect: () => containerRef.current?.getBoundingClientRect(),
getParentBoundingClientRect: () => containerRef?.current?.parentElement?.getBoundingClientRect(),
drawSelectionBox: (box: SelectionBox) => {
requestAnimationFrame(() => {
if (selectionBoxRef.current) {
const styles: React.CSSProperties = {
border: '1px solid #4C85D8',
background: 'rgba(155, 193, 239, 0.4)',
position: 'absolute',
pointerEvents: 'none',
...style,
top: box.top,
left: box.left,
width: box.width,
height: box.height,
};
selectionBoxRef.current.style.cssText = styleObjectToCSS(styles);
}
});
},
clearSelectionBox: () => {
requestAnimationFrame(() => {
if (selectionBoxRef.current) {
const styles: React.CSSProperties = {
top: 0,
left: 0,
width: 0,
height: 0,
};
selectionBoxRef.current.style.cssText = styleObjectToCSS(styles);
}
});
},
}),
);
useEffect(() => {
setForceUpdate((number) => number + 1);
}, []);
return (
<div ref={containerRef}>
{containerRef.current
? ReactDOM.createPortal(<div ref={selectionBoxRef} {...props} />, containerRef.current)
: null}
</div>
);
});
================================================
FILE: src/hooks/useSelectionContainer.tsx
================================================
import React, { ReactElement, useCallback, useRef } from 'react';
import { SelectionContainer, SelectionContainerProps } from '../components/SelectionContainer';
import { SelectionContainerRef } from '../utils/types';
import { useSelectionLogic, UseSelectionLogicParams } from './useSelectionLogic';
export interface UseSelectionContainerResult {
/**
* method to cancel current selecting
*/
cancelCurrentSelection: ReturnType<typeof useSelectionLogic>['cancelCurrentSelection'];
/**
* ReactNode which displays mouse selection. It should be rendered at the top of container of elements we want to select
*/
DragSelection: () => ReactElement;
}
export interface UseSelectionContainerParams<T extends HTMLElement>
extends Pick<
UseSelectionLogicParams<T>,
| 'onSelectionChange'
| 'onSelectionEnd'
| 'onSelectionStart'
| 'isEnabled'
| 'eventsElement'
| 'shouldStartSelecting'
| 'isValidSelectionStart'
> {
/** These are props that get passed to the selection box component (where styling gets passed in) */
selectionProps?: SelectionContainerProps;
}
/**
* Use this hook to enable mouse selection on a container.
* To prevent interfering with drag-n-drop feature, add data-draggable='true' to draggable item. Selection won't fire when click happens on that element
*/
export function useSelectionContainer<T extends HTMLElement>(
props?: UseSelectionContainerParams<T>,
): UseSelectionContainerResult {
const {
onSelectionChange,
onSelectionEnd,
onSelectionStart,
isEnabled = true,
selectionProps = {},
eventsElement,
shouldStartSelecting,
isValidSelectionStart,
} = props || {};
const containerRef = useRef<SelectionContainerRef>(null);
const { cancelCurrentSelection } = useSelectionLogic({
containerRef,
onSelectionEnd,
onSelectionStart,
onSelectionChange,
isEnabled,
eventsElement,
shouldStartSelecting,
isValidSelectionStart,
});
const DragSelection = useCallback(() => <SelectionContainer ref={containerRef} {...selectionProps} />, []);
return {
cancelCurrentSelection,
DragSelection,
};
}
================================================
FILE: src/hooks/useSelectionLogic.ts
================================================
import { RefObject, useCallback, useEffect, useRef } from 'react';
import { SelectionContainerRef, OnSelectionChange, Point, SelectionBox, Box } from '../utils/types';
import { calculateBoxArea, calculateSelectionBox } from '../utils/boxes';
export interface UseSelectionLogicResult {
cancelCurrentSelection: () => void;
}
export interface UseSelectionLogicParams<T extends HTMLElement> {
/** This callback will fire when the user starts selecting */
onSelectionStart?: (event: MouseEvent) => void;
/** This callback will fire when the user finishes selecting */
onSelectionEnd?: (event: MouseEvent) => void;
/** This callback will fire when the user's mouse changes position while selecting using requestAnimationFrame */
onSelectionChange?: OnSelectionChange;
/** This boolean enables selecting */
isEnabled?: boolean;
/** This is an HTML element that the mouse events (mousedown, mouseup, mousemove) should be attached to. Defaults to the document.body */
eventsElement?: T | null;
/** This is the ref of the parent of the selection box */
containerRef: RefObject<SelectionContainerRef | null>;
/**
* If supplied, this callback is fired on mousedown and can be used to prevent selection from starting.
* This is useful when you want to prevent certain areas of your application from being able to be selected.
* Returning true will enable selection and returning false will prevent selection from starting.
*
* @param {EventTarget | null} target - The element the mousedown event fired on when the user started selected
*/
shouldStartSelecting?: (target: EventTarget | null) => boolean;
/**
* Determines whether a selection's dimensions meet the criteria for initiating a selection.
* The purpose is to distinguish between clicks and the start of a selection gesture.
*
* The default implementation checks if the area of the box (width * height) is greater than 10.
*
* @returns `true` if the box dimensions meet the threshold for starting a selection, otherwise `false`.
*/
isValidSelectionStart?: (box: Box) => boolean;
}
/**
* This hook contains logic for selecting. It starts 'selection' on mousedown event and finishes it on mouseup event.
* When mousemove event is detected and user is selecting, it calls onSelectionChange and containerRef.drawSelectionBox
*/
export function useSelectionLogic<T extends HTMLElement>({
containerRef,
onSelectionChange,
onSelectionStart,
onSelectionEnd,
isEnabled = true,
eventsElement,
shouldStartSelecting,
isValidSelectionStart = isMinumumBoxArea,
}: UseSelectionLogicParams<T>): UseSelectionLogicResult {
const startPoint = useRef<null | Point>(null);
const endPoint = useRef<null | Point>(null);
const isSelecting = useRef(false);
// these are used in listeners attached to eventsElement. They are used as refs to ensure we always use the latest version
const currentSelectionChange = useRef(onSelectionChange);
const currentSelectionStart = useRef(onSelectionStart);
const currentSelectionEnd = useRef(onSelectionEnd);
const onChangeRefId = useRef<number | undefined>(undefined);
const isEnabledRef = useRef(isEnabled);
currentSelectionChange.current = useCallback(
(box: Box) => {
onChangeRefId.current = onSelectionChange
? requestAnimationFrame(() => {
onSelectionChange(box);
})
: undefined;
},
[onSelectionChange],
);
currentSelectionStart.current = onSelectionStart;
currentSelectionEnd.current = onSelectionEnd;
isEnabledRef.current = isEnabled;
/**
* Method to cancel selecting and reset internal data
*/
const cancelCurrentSelection = useCallback(() => {
startPoint.current = null;
endPoint.current = null;
isSelecting.current = false;
containerRef.current?.clearSelectionBox();
if (typeof onChangeRefId.current === 'number') {
cancelAnimationFrame(onChangeRefId.current);
}
}, [containerRef]);
/**
* method to calculate point from event in context of the whole screen
*/
const getPointFromEvent = useCallback(
(event: MouseEvent): Point => {
const rect = containerRef.current?.getParentBoundingClientRect();
return {
x: event.clientX - (typeof rect?.left === 'number' ? rect.left : 0),
y: event.clientY - (typeof rect?.top === 'number' ? rect.top : 0),
};
},
[containerRef],
);
/**
* Method called on mousemove event
*/
const handleMouseMove = useCallback(
(event: MouseEvent, rect?: DOMRect) => {
if (startPoint.current && endPoint.current) {
if (!rect) {
return;
}
const newSelectionBox = calculateSelectionBox({
startPoint: startPoint.current,
endPoint: endPoint.current,
});
// calculate box in context of container to compare with items' coordinates
const boxInContainer: SelectionBox = {
...newSelectionBox,
top: newSelectionBox.top + (rect?.top || 0),
left: newSelectionBox.left + (rect?.left || 0),
};
// we detect move only after some small movement
if (isValidSelectionStart(newSelectionBox)) {
if (!isSelecting.current) {
if (currentSelectionStart?.current) {
currentSelectionStart.current(event);
}
isSelecting.current = true;
}
containerRef.current?.drawSelectionBox(newSelectionBox);
currentSelectionChange.current?.(boxInContainer);
} else if (isSelecting.current) {
currentSelectionChange.current?.(boxInContainer);
}
} else {
cancelCurrentSelection();
}
},
[cancelCurrentSelection, containerRef],
);
const onMouseMove = useCallback(
(event: MouseEvent) => {
if (!startPoint.current) {
return;
}
const rect = containerRef.current?.getParentBoundingClientRect();
endPoint.current = getPointFromEvent(event);
handleMouseMove(event, rect);
},
[handleMouseMove, getPointFromEvent, containerRef],
);
const onMouseUp = useCallback(
(event: MouseEvent) => {
/**
* handle only left button up event
*/
if (event.button === 0) {
/**
* If the user just clicked down and up in the same place without dragging,
* we don't want to fire the onSelectionEnd event. We can do this
* by checking if endPoint.current exists.
*/
if (endPoint.current) {
currentSelectionEnd.current?.(event);
}
cancelCurrentSelection();
document.body.style.removeProperty('user-select');
document.body.style.removeProperty('-webkit-user-select');
(eventsElement || document.body).removeEventListener('mousemove', onMouseMove);
window?.removeEventListener('mouseup', onMouseUp);
}
},
[eventsElement, cancelCurrentSelection, onMouseMove],
);
const onMouseDown = useCallback(
(e: MouseEvent) => {
// handle only left button click
if (e.button === 0 && isEnabledRef.current) {
if (typeof shouldStartSelecting === 'function' && !shouldStartSelecting(e.target)) {
return;
}
// disable text selection for all document
document.body.style.userSelect = 'none';
document.body.style.webkitUserSelect = 'none';
startPoint.current = getPointFromEvent(e);
(eventsElement || document.body).addEventListener('mousemove', onMouseMove);
window?.addEventListener('mouseup', onMouseUp);
}
},
[eventsElement, getPointFromEvent, onMouseMove, onMouseUp],
);
useEffect(() => {
/**
* On mount, add the mouse down listener to begin listening for dragging
*/
(eventsElement || document.body).addEventListener('mousedown', onMouseDown);
/**
* On unmount, remove any listeners that we're applied.
*/
return () => {
(eventsElement || document.body).removeEventListener('mousedown', onMouseDown);
(eventsElement || document.body).removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
}, [eventsElement, onMouseDown, onMouseMove, onMouseUp]);
return {
cancelCurrentSelection,
};
}
function isMinumumBoxArea(box: Box): boolean {
return calculateBoxArea(box) > 10;
}
================================================
FILE: src/index.ts
================================================
import { SelectionContainerProps } from './components/SelectionContainer';
import { useSelectionContainer } from './hooks/useSelectionContainer';
import { boxesIntersect } from './utils/boxes';
import { Point, Box, SelectionBox, OnSelectionChange, SelectionContainerRef } from './utils/types';
export type { Point, Box, SelectionBox, OnSelectionChange, SelectionContainerRef, SelectionContainerProps };
export { useSelectionContainer, boxesIntersect };
================================================
FILE: src/typings.d.ts
================================================
/**
* Default CSS definition for typescript,
* will be overridden with file-specific definitions by rollup
*/
declare module '*.css' {
const content: { [className: string]: string };
export default content;
}
interface SvgrComponent extends React.FC<React.SVGAttributes<SVGElement>> {}
declare module '*.svg' {
const svgUrl: string;
const svgComponent: SvgrComponent;
export default svgUrl;
export { svgComponent as ReactComponent };
}
================================================
FILE: src/utils/__tests__/boxes.test.ts
================================================
import { boxesIntersect, calculateBoxArea } from '../boxes';
describe('boxes utils', () => {
describe('boxesIntersect', () => {
it('should return true if boxes overlap', () => {
expect(
boxesIntersect(
{
left: 0,
top: 0,
width: 3,
height: 3,
},
{
left: 2,
top: 2,
width: 3,
height: 3,
},
),
).toBe(true);
expect(
boxesIntersect(
{
left: 0,
top: 2,
width: 3,
height: 3,
},
{
left: 2,
top: 0,
width: 3,
height: 3,
},
),
).toBe(true);
expect(
boxesIntersect(
{
left: 2,
top: 2,
width: 3,
height: 3,
},
{
left: 0,
top: 0,
width: 3,
height: 3,
},
),
).toBe(true);
expect(
boxesIntersect(
{
left: 2,
top: 0,
width: 3,
height: 3,
},
{
left: 0,
top: 2,
width: 3,
height: 3,
},
),
).toBe(true);
expect(
boxesIntersect(
{
left: 2,
top: 2,
width: 3,
height: 3,
},
{
left: 1,
top: 1,
width: 6,
height: 3,
},
),
).toBe(true);
expect(
boxesIntersect(
{
left: 1,
top: 0,
width: 4,
height: 4,
},
{
left: 3,
top: 1,
width: 1,
height: 1,
},
),
).toBe(true);
expect(
boxesIntersect(
{
left: 3,
top: 1,
width: 1,
height: 1,
},
{
left: 1,
top: 0,
width: 4,
height: 4,
},
),
).toBe(true);
});
it('should return false if boxes do not overlap', () => {
expect(
boxesIntersect(
{
left: 0,
top: 0,
width: 20,
height: 20,
},
{
left: 30,
top: 0,
width: 20,
height: 20,
},
),
).toBe(false);
});
});
describe('calculateBoxArea', () => {
it('should calculate correct box area', () => {
const area = calculateBoxArea({
left: 2,
top: 3,
height: 4,
width: 5,
});
expect(area).toBe(20);
});
});
});
================================================
FILE: src/utils/boxes.ts
================================================
import { Box, Point } from './types';
/** This method returns true if two boxes intersects
* @param boxA
* @param boxB
*/
export const boxesIntersect = (boxA: Box, boxB: Box) =>
boxA.left <= boxB.left + boxB.width &&
boxA.left + boxA.width >= boxB.left &&
boxA.top <= boxB.top + boxB.height &&
boxA.top + boxA.height >= boxB.top;
export const calculateSelectionBox = ({ startPoint, endPoint }: { startPoint: Point; endPoint: Point }): Box => ({
left: Math.min(startPoint.x, endPoint.x),
top: Math.min(startPoint.y, endPoint.y),
width: Math.abs(startPoint.x - endPoint.x),
height: Math.abs(startPoint.y - endPoint.y),
});
export const calculateBoxArea = (box: Box) => box.width * box.height;
================================================
FILE: src/utils/types.ts
================================================
export interface Point {
x: number;
y: number;
}
export interface Box {
left: number;
top: number;
width: number;
height: number;
}
export interface SelectionBox extends Box {}
export type OnSelectionChange = (box: SelectionBox) => void;
export interface SelectionContainerRef {
drawSelectionBox: OnSelectionChange;
clearSelectionBox: () => void;
getBoundingClientRect: () => DOMRect | undefined;
getParentBoundingClientRect: () => DOMRect | undefined;
}
================================================
FILE: tsconfig.eslint.json
================================================
{
"extends": "./tsconfig.json",
"include": ["src", ".eslintrc.js"]
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"outDir": "dist",
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"jsxFactory": "",
"jsxFragmentFactory": ""
},
"include": [
"src"
]
}
gitextract_jvshjrpn/ ├── .eslintignore ├── .eslintrc.cjs ├── .github/ │ └── workflows/ │ ├── e2e-tests.yml │ ├── publish.yml │ └── unit-tests.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── cypress/ │ ├── e2e/ │ │ └── selecting.cy.js │ └── support/ │ ├── commands.ts │ └── e2e.ts ├── cypress.config.ts ├── example/ │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public/ │ │ ├── index.html │ │ ├── manifest.json │ │ └── robots.txt │ ├── src/ │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ └── setupTests.ts │ └── tsconfig.json ├── jest.config.cjs ├── package.json ├── src/ │ ├── components/ │ │ └── SelectionContainer.tsx │ ├── hooks/ │ │ ├── useSelectionContainer.tsx │ │ └── useSelectionLogic.ts │ ├── index.ts │ ├── typings.d.ts │ └── utils/ │ ├── __tests__/ │ │ └── boxes.test.ts │ ├── boxes.ts │ └── types.ts ├── tsconfig.eslint.json └── tsconfig.json
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (48K chars).
[
{
"path": ".eslintignore",
"chars": 93,
"preview": "build/\ndist/\nnode_modules/\n.snapshots/\n*.min.js\ncypress\ncypress.config.ts\nexample/\ncoverage/\n"
},
{
"path": ".eslintrc.cjs",
"chars": 609,
"preview": "module.exports = {\n extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommend..."
},
{
"path": ".github/workflows/e2e-tests.yml",
"chars": 1189,
"preview": "name: End-to-end tests\non: [push]\njobs:\n tests:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n us..."
},
{
"path": ".github/workflows/publish.yml",
"chars": 1275,
"preview": "name: Publish to npmjs\non:\n release:\n types: [created]\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n -..."
},
{
"path": ".github/workflows/unit-tests.yml",
"chars": 617,
"preview": "name: Unit tests\non: [push]\njobs:\n tests:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: ac..."
},
{
"path": ".gitignore",
"chars": 330,
"preview": "\n# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\nnode_modules\n\n# builds\nbuild..."
},
{
"path": ".nvmrc",
"chars": 7,
"preview": "22.16.0"
},
{
"path": ".prettierrc",
"chars": 116,
"preview": "{\n \"arrowParens\": \"always\",\n \"printWidth\": 120,\n \"semi\": true,\n \"singleQuote\": true,\n \"trailingComma\": \"all\"\n}\n"
},
{
"path": "LICENSE",
"chars": 1060,
"preview": "MIT License\n\nCopyright (c) 2020 Air\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof thi..."
},
{
"path": "README.md",
"chars": 5084,
"preview": "<p align=\"center\">\n <img style=\"width: 800px\" src=\"https://user-images.githubusercontent.com/1065539/184370954-6398b161..."
},
{
"path": "cypress/e2e/selecting.cy.js",
"chars": 1500,
"preview": "/// <reference types=\"cypress\" />\n\ndescribe('react-drag-to-select example', () => {\n beforeEach(() => {\n cy.visit('/..."
},
{
"path": "cypress/support/commands.ts",
"chars": 1314,
"preview": "/// <reference types=\"cypress\" />\n// ***********************************************\n// This example commands.ts shows y..."
},
{
"path": "cypress/support/e2e.ts",
"chars": 667,
"preview": "// ***********************************************************\n// This example support/e2e.ts is processed and\n// loaded..."
},
{
"path": "cypress.config.ts",
"chars": 211,
"preview": "import { defineConfig } from 'cypress';\n\nexport default defineConfig({\n e2e: {\n baseUrl: 'http://localhost:3000',..."
},
{
"path": "example/.gitignore",
"chars": 310,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn..."
},
{
"path": "example/README.md",
"chars": 273,
"preview": "This example was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).\n\nIt is linked to th..."
},
{
"path": "example/package.json",
"chars": 1166,
"preview": "{\n \"name\": \"example\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"dependencies\": {\n \"@air/react-drag-to-select\": \"fi..."
},
{
"path": "example/public/index.html",
"chars": 1721,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.i..."
},
{
"path": "example/public/manifest.json",
"chars": 492,
"preview": "{\n \"short_name\": \"React App\",\n \"name\": \"Create React App Sample\",\n \"icons\": [\n {\n \"src\": \"favicon.ico\",..."
},
{
"path": "example/public/robots.txt",
"chars": 67,
"preview": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
},
{
"path": "example/src/App.css",
"chars": 564,
"preview": ".App {\n text-align: center;\n}\n\n.App-logo {\n height: 40vmin;\n pointer-events: none;\n}\n\n@media (prefers-reduced-motion:..."
},
{
"path": "example/src/App.tsx",
"chars": 2681,
"preview": "import { useEffect, useRef, useState } from 'react';\nimport './App.css';\nimport { Box, boxesIntersect, useSelectionConta..."
},
{
"path": "example/src/index.css",
"chars": 717,
"preview": "body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Can..."
},
{
"path": "example/src/index.tsx",
"chars": 554,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport './index.css';\nimport App from './App';\nimpor..."
},
{
"path": "example/src/react-app-env.d.ts",
"chars": 40,
"preview": "/// <reference types=\"react-scripts\" />\n"
},
{
"path": "example/src/reportWebVitals.ts",
"chars": 425,
"preview": "import { ReportHandler } from 'web-vitals';\n\nconst reportWebVitals = (onPerfEntry?: ReportHandler) => {\n if (onPerfEntr..."
},
{
"path": "example/src/setupTests.ts",
"chars": 241,
"preview": "// jest-dom adds custom jest matchers for asserting on DOM nodes.\n// allows you to do things like:\n// expect(element).to..."
},
{
"path": "example/tsconfig.json",
"chars": 535,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es5\",\n \"lib\": [\n \"dom\",\n \"dom.iterable\",\n \"esnext\"\n ],..."
},
{
"path": "jest.config.cjs",
"chars": 211,
"preview": "/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */\nmodule.exports = {\n preset: 'ts-jest',\n testEnvironme..."
},
{
"path": "package.json",
"chars": 1570,
"preview": "{\n \"name\": \"@air/react-drag-to-select\",\n \"version\": \"5.0.11\",\n \"description\": \"A performant React library which adds..."
},
{
"path": "src/components/SelectionContainer.tsx",
"chars": 2253,
"preview": "import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';\nimport ReactDOM from 'react..."
},
{
"path": "src/hooks/useSelectionContainer.tsx",
"chars": 2152,
"preview": "import React, { ReactElement, useCallback, useRef } from 'react';\nimport { SelectionContainer, SelectionContainerProps }..."
},
{
"path": "src/hooks/useSelectionLogic.ts",
"chars": 8408,
"preview": "import { RefObject, useCallback, useEffect, useRef } from 'react';\nimport { SelectionContainerRef, OnSelectionChange, Po..."
},
{
"path": "src/index.ts",
"chars": 455,
"preview": "import { SelectionContainerProps } from './components/SelectionContainer';\nimport { useSelectionContainer } from './hook..."
},
{
"path": "src/typings.d.ts",
"chars": 453,
"preview": "/**\n * Default CSS definition for typescript,\n * will be overridden with file-specific definitions by rollup\n */\ndeclare..."
},
{
"path": "src/utils/__tests__/boxes.test.ts",
"chars": 2868,
"preview": "import { boxesIntersect, calculateBoxArea } from '../boxes';\n\ndescribe('boxes utils', () => {\n describe('boxesIntersect..."
},
{
"path": "src/utils/boxes.ts",
"chars": 713,
"preview": "import { Box, Point } from './types';\n\n/** This method returns true if two boxes intersects\n * @param boxA\n * @param box..."
},
{
"path": "src/utils/types.ts",
"chars": 479,
"preview": "export interface Point {\n x: number;\n y: number;\n}\n\nexport interface Box {\n left: number;\n top: number;\n width: num..."
},
{
"path": "tsconfig.eslint.json",
"chars": 73,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"include\": [\"src\", \".eslintrc.js\"]\n}\n"
},
{
"path": "tsconfig.json",
"chars": 584,
"preview": "{\n \"compilerOptions\": {\n \"outDir\": \"dist\",\n \"target\": \"es5\",\n \"lib\": [\n \"dom\",\n \"dom.iterable\",..."
}
]
About this extraction
This page contains the full source code of the AirLabsTeam/react-drag-to-select GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (43.0 KB), approximately 11.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.