diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 84c3eb0..f4cc808 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,6 @@ ## Contribution Guidelines -### Issue +### Opening an issue * Make sure you're using the latest version, check [releases](https://github.com/simonwep/viselect/releases/tag/2.1.2) for that. * [Use the search](https://github.com/simonwep/viselect/search?type=Issues), maybe there is already an answer. @@ -8,15 +8,16 @@ *** -### Pull Request +### Opening a pull request * Pull requests only into the [master](https://github.com/simonwep/viselect/tree/master) branch. +* Run `pnpm test:ci` before opening a pull request to verify that everything is working as expected. * Make sure to use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/): - - `docs: ` - for changes in the documentation. - - `feat([package]): ` - for new features in the corresponding package. - - `fix([package]): ` - for bug fixes in the corresponding package. - - `refactor([package]): ` - for changes in the code that neither fixes a bug nor adds a feature. - - `chore: ` - for changes in the build process or auxiliary tools. + - `docs: ` - for changes in the documentation. + - `feat([package]): ` - for new features in the corresponding package. + - `fix([package]): ` - for bug fixes in the corresponding package. + - `refactor([package]): ` - for changes in the code that neither fixes a bug nor adds a feature. + - `chore: ` - for changes in the build process or auxiliary tools. *** @@ -27,9 +28,12 @@ This project requires [pnpm](https://pnpm.io) and [node](https://nodejs.org/en/) 1. Fork this repo on [GitHub](https://github.com/simonwep/viselect). 2. Check out the master locally. 3. From your local repro run `pnpm install`. -4. Run `pnpm start dev` to start a dev server for all packages. +4. Run `pnpm dev` to start a dev server for all packages. +5. Make sure to run `pnpm test:ci` before opening a pull request, to verify that everything is working as expected. -#### Working on the docs +*** + +### Working on the docs This project uses [vitepress](https://vitepress.dev/) for the documentation. Use the `docs:` commands to work on the documentation. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index f801d23..0febc63 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ -github: Simonwep +github: simonwep patreon: simonwep custom: ["paypal.me/simonreinisch", "buymeacoffee.com/aVc3krbXQ"] diff --git a/README.md b/README.md index f864580..c1519fd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Logo + Logo

@@ -19,9 +19,12 @@ Buy me a coffee - Build Status + src="https://github.com/simonwep/viselect/actions/workflows/main.yml/badge.svg"> + Docs gzip size brotli size React support - Svelte support

### Features 🀘 @@ -47,56 +47,11 @@ * βœ” Zero dependencies * πŸ“± Mobile / touch support * πŸ–± Vertical and horizontal scroll support -* πŸ’ͺ Hardened (over 3 years old and used in many apps) +* πŸ’ͺ Battle tested (over 6 years old and used in many apps) ### Getting started -> [!NOTE] -> I'm currently in the middle of moving the migration from readmes to a new docs websites. -> Stay tuned for a better, more detailed documentation in the coming weeks! - -Check out the documentation for the package you want to use: - -* [@viselect/vanilla](packages/vanilla) - To be used with plain [JavaScript](http://vanilla-js.com/) or [TypeScript](https://www.typescriptlang.org/). -* [@viselect/preact](packages/preact) - [Preact](https://preactjs.com/) wrapper. -* [@viselect/react](packages/react) - [React](https://reactjs.org/) wrapper. -* [@viselect/vue](packages/vue) - [Vue3](https://v3.vuejs.org/) wrapper. - -> Check out [recipes](packages/vanilla/recipes.md) for commonly asked questions and how to solve them using the standard library! -> For information about events and more check out the [vanilla readme](packages/vanilla/README.md)! - -### Browser support - -This library will always produce an ESNext bundle. -If you want to support legacy browsers, please use the feature of your bundler to transpile dependencies. -In case of webpack and babel (give [vite](https://vitejs.dev/) a try, it's awesome) you'll have to install corresponding plugins such as [babel-plugin-proposal-optional-chaining](https://babeljs.io/docs/en/babel-plugin-proposal-optional-chaining) and include the dependency from `node_modules` which is normally entirely excluded from being processed. - -I do this to provide maximum flexibility and give those who target ESNext a chance to make full use of how this library is bundled. -Everything else is just a matter of configuration :) - -### Is this library the right choice for me? - -Viselect primarily focuses on being a high-performant engine to select elements with various boundaries, behaviors, and modes in your browser. -Viselect is to "full-blown libraries" what is [popper.js](https://popper.js.org/) to [tippy.js](https://atomiks.github.io/tippyjs/) - the _core_ of your feature. - -### Development - -Use the following commands to work on this locally (we use [lerna](https://lerna.js.org/) to manage this): - -* `npm run dev` _- Spawns a dev-server for all packages. Every framework-dependent package is bundled with the vanilla version._ -* `npm run build` _- Builds all packages in parallel._ -* `npm run lint:fix` _- Lints and fixes all errors in all packages._ - -For the development servers [vite](https://vitejs.dev/) is used. It's superb, you should give it a try. -To bundle it, we use [rollup](https://rollupjs.org/) (which is btw also used by vite behind the scenes) to have full control over how the bundle looks like. - -### Releasing a new version - -This project is managed via [lerna](https://lerna.js.org/). -To bump the version and publish a new one run the following commands: - -* `lerna version` -* `lerna publish from-package` +Head over to [the documentation](https://simonwep.github.io/viselect) to get started πŸš€ ### You want to contribute? diff --git a/packages/preact/README.md b/packages/preact/README.md index a71f310..2fe9ddd 100644 --- a/packages/preact/README.md +++ b/packages/preact/README.md @@ -45,86 +45,6 @@
-> [!NOTE] -> This is merely a convenience wrapper around [@viselect/vanilla](https://github.com/simonwep/viselect/tree/master/packages/vanilla). -> The core API is fairly simple, if you want to have full control over it, you should roll out your own wrapper in your app. +### Getting started -### Installation - -Install using your package manager of choice: - -``` -npm install @viselect/preact -``` - -Last but not least, you'll need to add some basic styles to make your selection-area visible: - -```css -.selection-area { - background: rgba(46, 115, 252, 0.11); - border: 2px solid rgba(98, 155, 255, 0.81); - border-radius: 0.1em; -} -``` - -Additionally, to not interfere with text-selection, selection-js won't prevent any default events anymore (as of `v2.0.3`). -This, however, can cause problems with the actual selection ("introduced" by [#99](https://github.com/simonwep/viselect/pull/99), reported in [#103](https://github.com/simonwep/viselect/issues/103)). -If you don't care about text-selection, add the following to the container where all your selectables are located: - -```css -.container { - user-select: none; -} -``` - -### Usage - -> [!NOTE] -> All options are exposed as props. -> They're a one-to-one mapping of the original options found in the [vanilla](../vanilla#configuration) version! - -```tsx -import {SelectionArea, SelectionEvent} from '@viselect/preact'; -import {FunctionalComponent} from 'preact'; -import {useState} from 'preact/hooks'; - -const App: FunctionComponent = () => { - const [selected, setSelected] = useState>(() => new Set()); - - const extractIds = (els: Element[]): number[] => - els.map(v => v.getAttribute('data-key')) - .filter(Boolean) - .map(Number); - - const onStart = ({event, selection}: SelectionEvent) => { - if (!event?.ctrlKey && !event?.metaKey) { - selection.clearSelection(); - setSelected(() => new Set()); - } - }; - - const onMove = ({store: {changed: {added, removed}}}: SelectionEvent) => { - setSelected(prev => { - const next = new Set(prev); - extractIds(added).forEach(id => next.add(id)); - extractIds(removed).forEach(id => next.delete(id)); - return next; - }); - }; - - return ( - <> - - {new Array(42).fill(0).map((_, index) => ( -
- ))} - - - ); -} -``` +Head over to [the documentation](https://simonwep.github.io/viselect) to get started πŸš€ diff --git a/packages/react/README.md b/packages/react/README.md index cbbeac6..2a33515 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -45,88 +45,6 @@
-> [!NOTE] -> This is merely a convenience wrapper around [@viselect/vanilla](https://github.com/simonwep/viselect/tree/master/packages/vanilla). -> The core API is fairly simple, if you want to have full control over it, you should roll out your own wrapper in your app. +### Getting started -### Installation - -Install using your package manager of choice: - -``` -npm install @viselect/react -``` - -> If you're (still) using CRA, you may run into issues while using the bundle provided. -> See [this comment](https://github.com/facebook/create-react-app/issues/8445#issuecomment-588545858) for how to fix it. - -Last but not least, you'll need to add some basic styles to make your selection-area visible: - -```css -.selection-area { - background: rgba(46, 115, 252, 0.11); - border: 2px solid rgba(98, 155, 255, 0.81); - border-radius: 0.1em; -} -``` - -Additionally, to not interfere with text-selection, selection-js won't prevent any default events anymore (as of `v2.0.3`). -This, however, can cause problems with the actual selection ("introduced" by [#99](https://github.com/simonwep/viselect/pull/99), reported in [#103](https://github.com/simonwep/viselect/issues/103)). -If you don't care about text-selection, add the following to the container where all your selectables are located: - -```css -.container { - user-select: none; -} -``` - -### Usage - -> [!NOTE] -> All options are exposed as props. -> They're a one-to-one mapping of the original options found in the [vanilla](../vanilla#configuration) version! - -```tsx -import {SelectionArea, SelectionEvent} from '@viselect/react'; -import React, {FunctionComponent, useState} from 'react'; - -const App: FunctionComponent = () => { - const [selected, setSelected] = useState>(() => new Set()); - - const extractIds = (els: Element[]): number[] => - els.map(v => v.getAttribute('data-key')) - .filter(Boolean) - .map(Number); - - const onStart = ({event, selection}: SelectionEvent) => { - if (!event?.ctrlKey && !event?.metaKey) { - selection.clearSelection(); - setSelected(() => new Set()); - } - }; - - const onMove = ({store: {changed: {added, removed}}}: SelectionEvent) => { - setSelected(prev => { - const next = new Set(prev); - extractIds(added).forEach(id => next.add(id)); - extractIds(removed).forEach(id => next.delete(id)); - return next; - }); - }; - - return ( - <> - - {new Array(42).fill(0).map((_, index) => ( -
- ))} - - - ); -} -``` +Head over to [the documentation](https://simonwep.github.io/viselect) to get started πŸš€ diff --git a/packages/vanilla/README.md b/packages/vanilla/README.md index d9606c5..0971cd4 100644 --- a/packages/vanilla/README.md +++ b/packages/vanilla/README.md @@ -45,247 +45,6 @@
-### Installation - -#### Via package manager - -Install using your package manager of choice: - -``` -npm install @viselect/vanilla -``` - -#### Via script tags - -```html - - -``` - -##### Via ES6 import - -```js -import SelectionArea from "https://cdn.jsdelivr.net/npm/@viselect/vanilla/dist/viselect.mjs" -``` - ### Getting started -Last but not least, you'll need to add some basic styles to make your selection-area visible: - -```css -.selection-area { - background: rgba(46, 115, 252, 0.11); - border: 2px solid rgba(98, 155, 255, 0.81); - border-radius: 0.1em; -} -``` - -Additionally, to not interfere with text-selection, selection-js won't prevent any default events anymore (as of `v2.0.3`). -This, however, can cause problems with the actual selection ("introduced" by [#99](https://github.com/simonwep/viselect/pull/99), reported in [#103](https://github.com/simonwep/viselect/issues/103)). -If you don't care about text-selection, add the following to the container where all your selectables are located: - -```css -.container { - user-select: none; -} -``` - -### Configuration - -```ts -const selection = new SelectionArea({ - - // Class for the selection-area itself (the element). - selectionAreaClass: 'selection-area', - - // Class for the selection-area container. - selectionContainerClass: 'selection-area-container', - - // Query selector or dom-node to set up container for the selection-area element. - container: 'body', - - // document object - if you want to use it within an embed document (or iframe). - // If you're inside of a shadow-dom make sure to specify the shadow root here. - document: window.document, - - // Query selectors for elements which can be selected. - selectables: [], - - // Query selectors for elements from where a selection can be started from. - startareas: ['html'], - - // Query selectors for elements which will be used as boundaries for the selection. - // The boundary will also be the scrollable container if this is the case. - boundaries: ['html'], - - // Behaviour related options. - behaviour: { - - // Specifies what should be done if already selected elements get selected again. - // invert: Invert selection for elements which were already selected - // keep: Keep selected elements (use clearSelection() to remove those) - // drop: Remove stored elements after they have been touched - overlap: 'invert', - - // On which point an element should be selected. - // Available modes are cover (cover the entire element), center (touch the center) or - // the default mode is touch (just touching it). - intersect: 'touch', - - // px, how many pixels the point should move before starting the selection (combined distance). - // Or specifiy the threshold for each axis by passing an object like {x: , y: }. - startThreshold: 10, - - // List of triggers that should cause the selection to begin. - // Each element in the list can be one of the following - // - a MouseButton (numbers 0 through 4) - // see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button#value - // - an object of shape { button: MouseButton, modifiers: Modifier[] } - // where a Modifier is ( 'ctrl' | 'meta' | 'alt' | 'shift' ) - // - // To trigger the selection with e.g. OR the - // trigger property should look like - // - // triggers: [ { button: 0, modifiers: [ "ctrl", "shift" ] }, 2 ] - // - // The default value is [0], enabling only the main mouse button (usually left click). - // On mac the ctrl will act as the meta key. - triggers: [0], - - // Scroll configuration. - scrolling: { - - // On scrollable areas the number on px per frame is devided by this amount. - // Default is 10 to provide a enjoyable scroll experience. - speedDivider: 10, - - // Browsers handle mouse-wheel events differently, this number will be used as - // numerator to calculate the mount of px while scrolling manually: manualScrollSpeed / scrollSpeedDivider. - manualSpeed: 750, - - // This property defines the virtual inset margins from the borders of the container - // component that, when crossed by the mouse/touch, trigger the scrolling. Useful for - // fullscreen containers. - startScrollMargins: {x: 0, y: 0} - } - }, - - // Features. - features: { - - // Enable / disable touch support. - touch: true, - - // Range selection. - range: true, - - // De-select all if user clicks clicks outside selectables. - // Disabled by default because it is not possible to reliably detect if the user clicked on a scrollbar. - deselectOnBlur: false, - - // Configuration in case a selectable gets just clicked. - singleTap: { - - // Enable single-click selection (Also disables range-selection via shift + ctrl). - allow: true, - - // 'native' (element was mouse-event target) or 'touch' (element visually touched). - intersect: 'native' - } - } -}); - -``` - -## Events - -Use the `on(event, cb)` and `off(event, cb)` functions to bind / unbind event-listener. - - -| Event | Description | -|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `beforestart` | The user tapped one of the areas within the specified boundaries. Return `false` to cancel selection immediatly. | -| `beforedrag` | Same as `beforestart` but _before_ the user starts selecting by dragging the mouse. Can be used to conditionally allow a selection by dragging. Return `false` to cancel the selection. | -| `start` | Selection started, here you can decide if you want to keep your previously selected elements. | -| `move` | Selection is active, user is moving the pointer around. | -| `stop` | Selection has stopped. | - -### Functions - -| Function | Description | -|-----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| -| `resolveSelectables(): void` | Updates the list of selectables, useful if new elements have been added during a selection. | -| `getSelection(): Element[]` | Returns currently selected element. Use it in the `stop` event to collect selected elements. | -| `getSelectionArea(): HTMLElement` | Returns the selection area element. | -| `cancel(keepEvent = false): void` | Cancel the currently active selection, pass true to trigger the `stop` event afterwards. | -| `destroy(): void` | Destroy the `SelectionArea`-instance, removes all event-listeners and the selection-area element from the DOM. | -| `disable(): void` | Disables the selection-area temporarily. | -| `enable(): void` | Enables the selection-area. | -| `select(query: SelectAllSelectors, quiet = false): Element[]` | Manually select elements, if `quiet` is set to `true` this will not fire the `move` & `stop` event. | -| `deselect(query: SelectAllSelectors, quiet = false): Element[]` | Manually deselect elements, if `quiet` is set to `true` this will not fire the `move` & `stop` event. | -| `clearSelection(includeStored = true, quiet = false): void` | Clears the selection, pass `false` to keep previously selected elements. If `quiet` is set to `true` this will not fire the `move` & `stop` event. | -| `trigger(evt: MouseEvent / TouchEvent, silent = true): void` | Manually trigger a selection. | -| `getSelectables(): Element[]` | Returns all selectables. | -| `getAreaLocation(): AreaLocation` | Returns the current location of the selection area. | -| `setAreaLocation(location: AreaLocation): void` | Update the location of the selection area. | - - -### Example - -```ts -selection.on('beforestart', evt => { - - // Use this event to decide whether a selection should take place or not. - // For example if the user should be able to normally interact with input-elements you - // may want to prevent a selection if the user clicks such a element: - // selection.on('beforestart', ({event}) => { - // return event.target.tagName !== 'INPUT'; // Returning false prevents a selection - // }); - - console.log('beforestart', evt); -}).on('beforedrag', evt => { - - // Same as 'beforestart' but before a selection via dragging happens. - console.log('beforedrag', evt); -}).on('start', evt => { - - // A selection got initiated, you could now clear the previous selection or - // keep it if in case of multi-selection. - console.log('start', evt); -}).on('move', evt => { - - // Here you can update elements based on their state. - console.log('move', evt); -}).on('stop', evt => { - - // Do something with the selected elements. - console.log('stop', evt); -}); -``` - -#### Virtual / dynamic lists - -In some cases you may add / remove selectables during a selection. -Especially when it comes to scrolling. -In this case make sure to call `selection.resolveSelectables()` every time you add / remove a selectable so that viselect is aware of the change. - -### Event properties -Every event comes with the following properties: - -```typescript -{ - selection: SelectionArea // Current instance - event: TouchEvent | MouseEvent | null // TouchEvent, MouseEvent, or `null` if triggered manually - store: { - touched: Element[] // Touched elements - selected: Element[] // Elements from the currently active selection (each click, drag counts as a single "selection") - stored: Element[] // Elements currently selected (in total, not just an instant) - changed: { - added: Element[] // Added elements since last change - removed: Element[] // Removed elements since last change - } - } -} -``` - -> Common recipes can be found under [recipes](recipes.md). +Head over to [the documentation](https://simonwep.github.io/viselect) to get started πŸš€ diff --git a/packages/vanilla/recipes.md b/packages/vanilla/recipes.md deleted file mode 100644 index c0fb703..0000000 --- a/packages/vanilla/recipes.md +++ /dev/null @@ -1,79 +0,0 @@ -### Requested features - immediately brought to life by a bit of code - -#### Allowing the user to scroll with two fingers ([#70](https://github.com/simonwep/viselect/issues/70)) - -```js -selection.on('beforestart', (() => { - let timeout = null; - - return ({event}) => { - - // Check if user already tapped inside of a selection-area. - if (timeout !== null) { - - // A second pointer-event occured, ignore that one. - clearTimeout(timeout); - timeout = null; - } else { - - // Wait 50ms in case the user uses two fingers to scroll. - timeout = setTimeout(() => { - - // OK User used only one finger, we can safely initiate a selection and reset the timer. - selection.trigger(event); - timeout = null; - }, 50); - } - - // Never start automatically. - return false; - }; -})()); -``` - -#### Preventing the start of a selection based on certain conditions ([#73](https://github.com/simonwep/viselect/issues/73)) - -```js -selection.on('beforestart', ({event}) => { - return !event.path.some(item => { - - // item is in this case an element affected by the event-bubbeling. - // To exclude elements with class "blocked" you could do the following (#73): - return item.classList.contains('blocked'); - - // If the areas you're using contains input elements you might want to prevent - // any out-going selections from these elements (#72): - return event.target.tagName !== 'INPUT'; - }); -}); -``` - -#### Preventing select from right click, middle mouse or left click ([#101](https://github.com/simonwep/viselect/issues/101)) -```js -selection.on('beforestart', (event) => { - const allowedButtons = [ - // See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons - 1, // left click - 2, // right click - 4, // mouse wheel / middle button - ]; - - return allowedButtons.includes(event.event.buttons); -}); -``` - -> Feel free to submit a [PR](https://github.com/simonwep/viselect/compare) or create -> an [issue](https://github.com/simonwep/viselect/issues/new?assignees=Simonwep&labels=&template=feature_request.md&title=) if -> you got any ideas for more examples! - -#### Preventing text-selection - -As of v2.1.0, it will no longer prevent text-selection. -If this is wanted it can be done using two of the event hooks and a bit of css: - -```js -selection - .on('beforestart', () => document.body.style.userSelect = 'none') - .on('stop', () => document.body.style.userSelect = 'unset'); -``` - diff --git a/packages/vue/README.md b/packages/vue/README.md index f971e27..8118598 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -45,123 +45,6 @@
-> [!NOTE] -> This is merely a convenience wrapper around [@viselect/vanilla](https://github.com/simonwep/viselect/tree/master/packages/vanilla). -> The core API is fairly simple, if you want to have full control over it, you should roll out your own wrapper in your app. +### Getting started -### Installation - -Install using your package manager of choice: - -``` -npm install @viselect/vue -``` - -Last but not least, you'll need to add some basic styles to make your selection-area visible: - -```css -.selection-area { - background: rgba(46, 115, 252, 0.11); - border: 2px solid rgba(98, 155, 255, 0.81); - border-radius: 0.1em; -} -``` - -Additionally, to not interfere with text-selection, selection-js won't prevent any default events anymore (as of `v2.0.3`). -This, however, can cause problems with the actual selection ("introduced" by [#99](https://github.com/simonwep/viselect/pull/99), reported in [#103](https://github.com/simonwep/viselect/issues/103)). -If you don't care about text-selection, add the following to the container where all your selectables are located: - -```css -.container { - user-select: none; -} -``` - -### Usage - -> [!NOTE] -> All options are exposed as props. -> They're a one-to-one mapping of the original options found in the [vanilla](../vanilla#configuration) version! - -> Events are handled using props because you can’t return a value in events synchronously. - -```vue - - - -``` - -#### Component exposed API - -##### `selection` - -It's possible to get the current `SelectionArea`-instance via [template refs](https://vuejs.org/guide/essentials/template-refs.html). - -```vue - - - -``` +Head over to [the documentation](https://simonwep.github.io/viselect) to get started πŸš€