svelte-dnd-action
Advanced tools
Comparing version 0.6.20 to 0.6.21
@@ -7,3 +7,6 @@ /** | ||
*/ | ||
export declare function dndzone(node: HTMLElement, options: Options): { | ||
export declare function dndzone( | ||
node: HTMLElement, | ||
options: Options | ||
): { | ||
update: (newOptions: Options) => void; | ||
@@ -16,3 +19,3 @@ destroy: () => void; | ||
draggedElementData?: Item, // the data of the item from the items array | ||
index?: number, // the index the dragged element would get if dropped into the new dnd-zone | ||
index?: number // the index the dragged element would get if dropped into the new dnd-zone | ||
) => void; | ||
@@ -42,5 +45,3 @@ | ||
*/ | ||
export declare function overrideItemIdKeyNameBeforeInitialisingDndZones( | ||
newKeyName: string | ||
): void; | ||
export declare function overrideItemIdKeyNameBeforeInitialisingDndZones(newKeyName: string): void; | ||
@@ -55,3 +56,3 @@ export enum TRIGGERS { | ||
DROPPED_OUTSIDE_OF_ANY = "droppedOutsideOfAny", | ||
DRAG_STOPPED = "dragStopped", //only relevant for keyboard interactions - when the use exists dragging mode | ||
DRAG_STOPPED = "dragStopped" //only relevant for keyboard interactions - when the use exists dragging mode | ||
} | ||
@@ -71,5 +72,5 @@ | ||
export type DndEvent = { | ||
items: Item[], | ||
info: DndEventInfo | ||
} | ||
items: Item[]; | ||
info: DndEventInfo; | ||
}; | ||
@@ -81,2 +82,2 @@ export declare const SHADOW_ITEM_MARKER_PROPERTY_NAME: "isDndShadowItem"; | ||
*/ | ||
export declare function setDebug(isDebug: boolean): void; | ||
export declare function setDebug(isDebug: boolean): void; |
109
package.json
{ | ||
"name": "svelte-dnd-action", | ||
"svelte": "src/index.js", | ||
"module": "dist/index.mjs", | ||
"main": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"scripts": { | ||
"test": "cypress run", | ||
"lint": "eslint 'src/**'", | ||
"build": "npm run lint && rollup -c", | ||
"prepublishOnly": "npm run build" | ||
}, | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"@babel/core": "^7.9.6", | ||
"@babel/preset-env": "^7.9.6", | ||
"@rollup/plugin-node-resolve": "^6.0.0", | ||
"babel-jest": "^26.0.1", | ||
"cypress": "^4.5.0", | ||
"eslint": "^7.11.0", | ||
"rollup": "^1.20.0", | ||
"rollup-plugin-babel": "^4.3.2", | ||
"rollup-plugin-copy": "^3.3.0" | ||
}, | ||
"keywords": [ | ||
"svelte", | ||
"drag and drop", | ||
"sortable", | ||
"dnd", | ||
"draggable", | ||
"accessible", | ||
"touch" | ||
], | ||
"files": [ | ||
"src", | ||
"dist" | ||
], | ||
"description": "*An awesome drag and drop library for Svelte 3 (not using the browser's built-in dnd, thanks god): Rich animations, nested containers, touch support and more *", | ||
"version": "0.6.20", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/isaacHagoel/svelte-dnd-action.git" | ||
}, | ||
"author": "Isaac Hagoel", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/isaacHagoel/svelte-dnd-action/issues" | ||
}, | ||
"homepage": "https://github.com/isaacHagoel/svelte-dnd-action#readme" | ||
"name": "svelte-dnd-action", | ||
"svelte": "src/index.js", | ||
"module": "dist/index.mjs", | ||
"main": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"scripts": { | ||
"test": "cypress run", | ||
"lint": "eslint .", | ||
"format": "prettier --write .", | ||
"build": "yarn lint && rollup -c", | ||
"prepublishOnly": "yarn build" | ||
}, | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"@babel/core": "^7.9.6", | ||
"@babel/preset-env": "^7.9.6", | ||
"@rollup/plugin-node-resolve": "^6.0.0", | ||
"babel-jest": "^26.0.1", | ||
"cypress": "^4.5.0", | ||
"eslint": "^7.11.0", | ||
"husky": "^4.3.0", | ||
"lint-staged": "^10.5.1", | ||
"prettier": "^2.1.2", | ||
"rollup": "^1.20.0", | ||
"rollup-plugin-babel": "^4.3.2", | ||
"rollup-plugin-copy": "^3.3.0" | ||
}, | ||
"keywords": [ | ||
"svelte", | ||
"drag and drop", | ||
"sortable", | ||
"dnd", | ||
"draggable", | ||
"accessible", | ||
"touch" | ||
], | ||
"files": [ | ||
"src", | ||
"dist" | ||
], | ||
"description": "*An awesome drag and drop library for Svelte 3 (not using the browser's built-in dnd, thanks god): Rich animations, nested containers, touch support and more *", | ||
"version": "0.6.21", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/isaacHagoel/svelte-dnd-action.git" | ||
}, | ||
"author": "Isaac Hagoel", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/isaacHagoel/svelte-dnd-action/issues" | ||
}, | ||
"homepage": "https://github.com/isaacHagoel/svelte-dnd-action#readme", | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "lint-staged" | ||
} | ||
}, | ||
"lint-staged": { | ||
"*.js": "eslint --cache --fix", | ||
"*.{js,css,md}": "prettier --write" | ||
} | ||
} |
308
README.md
@@ -1,3 +0,4 @@ | ||
# SVELTE DND ACTION [![Known Vulnerabilities](https://snyk.io/test/github/isaacHagoel/svelte-dnd-action/badge.svg?targetFile=package.json)](https://snyk.io/test/github/isaacHagoel/svelte-dnd-action?targetFile=package.json) | ||
This is a feature-complete implementation of drag and drop for Svelte using a custom action. It supports almost every imaginable drag and drop use-case and is fully accessible. | ||
# SVELTE DND ACTION [![Known Vulnerabilities](https://snyk.io/test/github/isaacHagoel/svelte-dnd-action/badge.svg?targetFile=package.json)](https://snyk.io/test/github/isaacHagoel/svelte-dnd-action?targetFile=package.json) | ||
This is a feature-complete implementation of drag and drop for Svelte using a custom action. It supports almost every imaginable drag and drop use-case and is fully accessible. | ||
See full features list below. | ||
@@ -10,23 +11,29 @@ | ||
### Current Status | ||
The library is working well as far as I can tell, and I am in the process of integrating it into a production system that will be used at scale. | ||
The library is working well as far as I can tell, and I am in the process of integrating it into a production system that will be used at scale. | ||
It is being actively maintained. | ||
### Features | ||
- Awesome drag and drop with minimal fuss | ||
- Supports horizontal, vertical or any other type of container (it doesn't care much about the shape) | ||
- Supports nested dnd-zones (draggable containers with other draggable elements inside, think Trello) | ||
- Rich animations (can be opted out of) | ||
- Touch support | ||
- Define what can be dropped where (dnd-zones optionally have a "type") | ||
- Scroll dnd-zones and/or the window horizontally or vertically by placing the dragged element next to the edge | ||
- Supports advanced use-cases such as various flavours of copy-on-drag and custom drag handles (see examples below) | ||
- Performant and small footprint (no external dependencies, no fluff code) | ||
- Fully accessible (beta) - keyboard support, aria attributes and assistive instructions for screen readers | ||
- Awesome drag and drop with minimal fuss | ||
- Supports horizontal, vertical or any other type of container (it doesn't care much about the shape) | ||
- Supports nested dnd-zones (draggable containers with other draggable elements inside, think Trello) | ||
- Rich animations (can be opted out of) | ||
- Touch support | ||
- Define what can be dropped where (dnd-zones optionally have a "type") | ||
- Scroll dnd-zones and/or the window horizontally or vertically by placing the dragged element next to the edge | ||
- Supports advanced use-cases such as various flavours of copy-on-drag and custom drag handles (see examples below) | ||
- Performant and small footprint (no external dependencies, no fluff code) | ||
- Fully accessible (beta) - keyboard support, aria attributes and assistive instructions for screen readers | ||
### Installation | ||
**Pre-requisites**: svelte-3 | ||
```bash | ||
yarn add -D svelte-dnd-action | ||
``` | ||
or | ||
```bash | ||
@@ -37,8 +44,9 @@ npm install --save-dev svelte-dnd-action | ||
### Usage | ||
```html | ||
<div use:dndzone="{{items: myItems, ...otherOptions}}" on:consider={handler} on:finalize={handler}> | ||
{#each myItems as item(item.id)} | ||
<div>this is now a draggable div that can be dropped in other dnd zones</div> | ||
{/each} | ||
</div> | ||
<div use:dndzone="{{items: myItems, ...otherOptions}}" on:consider="{handler}" on:finalize="{handler}"> | ||
{#each myItems as item(item.id)} | ||
<div>this is now a draggable div that can be dropped in other dnd zones</div> | ||
{/each} | ||
</div> | ||
``` | ||
@@ -50,41 +58,39 @@ | ||
<script> | ||
import { flip } from 'svelte/animate'; | ||
import { dndzone } from 'svelte-dnd-action'; | ||
import {flip} from "svelte/animate"; | ||
import {dndzone} from "svelte-dnd-action"; | ||
let items = [ | ||
{id: 1, name: "item1"}, | ||
{id: 2, name: "item2"}, | ||
{id: 3, name: "item3"}, | ||
{id: 4, name: "item4"} | ||
]; | ||
{id: 1, name: "item1"}, | ||
{id: 2, name: "item2"}, | ||
{id: 3, name: "item3"}, | ||
{id: 4, name: "item4"} | ||
]; | ||
const flipDurationMs = 300; | ||
function handleDndConsider(e) { | ||
items = e.detail.items; | ||
} | ||
function handleDndFinalize(e) { | ||
items = e.detail.items; | ||
} | ||
function handleDndConsider(e) { | ||
items = e.detail.items; | ||
} | ||
function handleDndFinalize(e) { | ||
items = e.detail.items; | ||
} | ||
</script> | ||
<style> | ||
section { | ||
width: 50%; | ||
padding: 0.3em; | ||
border: 1px solid black; | ||
section { | ||
width: 50%; | ||
padding: 0.3em; | ||
border: 1px solid black; | ||
/* this will allow the dragged element to scroll the list */ | ||
overflow: scroll; | ||
height: 200px; | ||
} | ||
div { | ||
width: 50%; | ||
padding: 0.2em; | ||
border: 1px solid blue; | ||
margin: 0.15em 0; | ||
} | ||
overflow: scroll; | ||
height: 200px; | ||
} | ||
div { | ||
width: 50%; | ||
padding: 0.2em; | ||
border: 1px solid blue; | ||
margin: 0.15em 0; | ||
} | ||
</style> | ||
<section use:dndzone={{items, flipDurationMs}} on:consider={handleDndConsider} on:finalize={handleDndFinalize}> | ||
{#each items as item(item.id)} | ||
<div animate:flip="{{duration: flipDurationMs}}"> | ||
{item.name} | ||
</div> | ||
{/each} | ||
<section use:dndzone="{{items," flipDurationMs}} on:consider="{handleDndConsider}" on:finalize="{handleDndFinalize}"> | ||
{#each items as item(item.id)} | ||
<div animate:flip="{{duration: flipDurationMs}}">{item.name}</div> | ||
{/each} | ||
</section> | ||
@@ -94,13 +100,14 @@ ``` | ||
##### Input: | ||
An options-object with the following attributes: | ||
| Name | Type | Required? | Default Value | Description | | ||
| Name | Type | Required? | Default Value | Description | | ||
| ------------------------- | -------------- | ------------------------------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------ | | ||
| `items` | Array<Object> | Yes. Each object in the array **has to have** an `id` property (key name can be overridden globally) with a unique value (within all dnd-zones of the same type) | N/A | The data array that is used to produce the list with the draggable items (the same thing you run your #each block on) | | ||
| `flipDurationMs` | Number | No | `0` | The same value you give the flip animation on the items (to make them animated as they "make space" for the dragged item). Set to zero or leave out if you don't want animations | | ||
| `type` | String | No | Internal | dnd-zones that share the same type can have elements from one dragged into another. By default, all dnd-zones have the same type | | ||
| `dragDisabled` | Boolean | No | `false` | Setting it to true will make it impossible to drag elements out of the dnd-zone. You can change it at any time, and the zone will adjust on the fly | | ||
| `dropFromOthersDisabled` | Boolean | No | `false` | Setting it to true will make it impossible to drop elements from other dnd-zones of the same type. Can be useful if you want to limit the max number of items for example. You can change it at any time, and the zone will adjust on the fly | | ||
| `dropTargetStyle` | Object<String> | No | `{outline: 'rgba(255, 255, 102, 0.7) solid 2px'}` | An object of styles to apply to the dnd-zone when items can be dragged in to it. Note: the styles override any inline styles applied to the dnd-zone. When the styles are removed, any original inline styles will be lost | | ||
| `transformDraggedElement` | Function | No | `() => {}` | A function that is invoked when the draggable element enters the dnd-zone or hover overs a new index in the current dnd-zone. <br />Signature:<br />function(element, data, index) {}<br />**element**: The dragged element. <br />**data**: The data of the item from the items array.<br />**index**: The index the dragged element will become in the new dnd-zone.<br /><br />This allows you to override properties on the dragged element, such as innerHTML to change how it displays. | | ||
| `autoAriaDisabled` | Boolean | No | `false` | Setting it to true will disable all the automatically added aria attributes and aria alerts (for example when the user starts/ stops dragging using the keyboard).<br /> **Use it only if you intend to implement your own custom instructions, roles and alerts.** In such a case, you might find the exported function `alertToScreenReader(string)` useful. | | ||
| `items` | Array<Object> | Yes. Each object in the array **has to have** an `id` property (key name can be overridden globally) with a unique value (within all dnd-zones of the same type) | N/A | The data array that is used to produce the list with the draggable items (the same thing you run your #each block on) | | ||
| `flipDurationMs` | Number | No | `0` | The same value you give the flip animation on the items (to make them animated as they "make space" for the dragged item). Set to zero or leave out if you don't want animations | | ||
| `type` | String | No | Internal | dnd-zones that share the same type can have elements from one dragged into another. By default, all dnd-zones have the same type | | ||
| `dragDisabled` | Boolean | No | `false` | Setting it to true will make it impossible to drag elements out of the dnd-zone. You can change it at any time, and the zone will adjust on the fly | | ||
| `dropFromOthersDisabled` | Boolean | No | `false` | Setting it to true will make it impossible to drop elements from other dnd-zones of the same type. Can be useful if you want to limit the max number of items for example. You can change it at any time, and the zone will adjust on the fly | | ||
| `dropTargetStyle` | Object<String> | No | `{outline: 'rgba(255, 255, 102, 0.7) solid 2px'}` | An object of styles to apply to the dnd-zone when items can be dragged in to it. Note: the styles override any inline styles applied to the dnd-zone. When the styles are removed, any original inline styles will be lost | | ||
| `transformDraggedElement` | Function | No | `() => {}` | A function that is invoked when the draggable element enters the dnd-zone or hover overs a new index in the current dnd-zone. <br />Signature:<br />function(element, data, index) {}<br />**element**: The dragged element. <br />**data**: The data of the item from the items array.<br />**index**: The index the dragged element will become in the new dnd-zone.<br /><br />This allows you to override properties on the dragged element, such as innerHTML to change how it displays. | | ||
| `autoAriaDisabled` | Boolean | No | `false` | Setting it to true will disable all the automatically added aria attributes and aria alerts (for example when the user starts/ stops dragging using the keyboard).<br /> **Use it only if you intend to implement your own custom instructions, roles and alerts.** In such a case, you might find the exported function `alertToScreenReader(string)` useful. | | ||
@@ -110,13 +117,15 @@ ##### Output: | ||
The action dispatches two custom events: | ||
- `consider` - dispatched whenever the dragged element needs to make room for itself in a new position in the items list and when it leaves. The host (your component) is expected to update the items list (you can keep a copy of the original list if you need to) | ||
- `finalize` - dispatched on the target and origin dnd-zones when the dragged element is dropped into position. | ||
- `consider` - dispatched whenever the dragged element needs to make room for itself in a new position in the items list and when it leaves. The host (your component) is expected to update the items list (you can keep a copy of the original list if you need to) | ||
- `finalize` - dispatched on the target and origin dnd-zones when the dragged element is dropped into position. | ||
The expectation is the same - update the list of items. | ||
In both cases the payload (within e.detail) is the same: an object with two attributes: `items` and `info`. | ||
- `items`: contains the updated items list. | ||
- `info`: This one can be used to achieve very advanced custom behaviours (ex: copy on drag). In most cases, don't worry about it. It is an object with the following properties: | ||
* `trigger`: will be one of the exported list of TRIGGERS (Please import if you plan to use): [DRAG_STARTED, DRAGGED_ENTERED, DRAGGED_OVER_INDEX, DRAGGED_LEFT, DROPPED_INTO_ZONE, DROPPED_INTO_ANOTHER, DROPPED_OUTSIDE_OF_ANY, DRAG_STOPPED]. Most triggers apply to both pointer and keyboard, but some are only relevant for pointer (dragged_entered, dragged_over_index and dragged_left), and some only for keyboard (drag_stopped) | ||
* `id`: the item id of the dragged element | ||
* `source`: will be one of the exported list of SOURCES (Please import if you plan to use): [POINTER, KEYBOARD] | ||
- `items`: contains the updated items list. | ||
- `info`: This one can be used to achieve very advanced custom behaviours (ex: copy on drag). In most cases, don't worry about it. It is an object with the following properties: | ||
- `trigger`: will be one of the exported list of TRIGGERS (Please import if you plan to use): [DRAG_STARTED, DRAGGED_ENTERED, DRAGGED_OVER_INDEX, DRAGGED_LEFT, DROPPED_INTO_ZONE, DROPPED_INTO_ANOTHER, DROPPED_OUTSIDE_OF_ANY, DRAG_STOPPED]. Most triggers apply to both pointer and keyboard, but some are only relevant for pointer (dragged_entered, dragged_over_index and dragged_left), and some only for keyboard (drag_stopped) | ||
- `id`: the item id of the dragged element | ||
- `source`: will be one of the exported list of SOURCES (Please import if you plan to use): [POINTER, KEYBOARD] | ||
You have to listen for both events and update the list of items in order for this library to work correctly. | ||
@@ -127,124 +136,134 @@ | ||
### Accessibility (beta) | ||
If you want screen-readers to tell the user which item is being dragged and which container it interacts with, **please add `aria-label` on the container and on every draggable item**. The library will take care of the rest. | ||
For example: | ||
```html | ||
<h2>{listName}</h2> | ||
<section aria-label="{listName}" use:dndzone={{items, flipDurationMs}} on:consider={handleDndConsider} on:finalize={handleDndFinalize}> | ||
{#each items as item(item.id)} | ||
<div aria-label="{item.name}" animate:flip="{{duration: flipDurationMs}}"> | ||
{item.name} | ||
</div> | ||
{/each} | ||
<section aria-label="{listName}" use:dndzone="{{items," flipDurationMs}} on:consider="{handleDndConsider}" on:finalize="{handleDndFinalize}"> | ||
{#each items as item(item.id)} | ||
<div aria-label="{item.name}" animate:flip="{{duration: flipDurationMs}}">{item.name}</div> | ||
{/each} | ||
</section> | ||
``` | ||
If you don't provide the aria-labels everything will still work, but the messages to the user will be less informative. | ||
*Note*: in general you probably want to use semantic-html (ex: `ol` and `li` elements rather than `section` and `div`) but the library is screen readers friendly regardless (or at least that's the goal :)). | ||
If you want to implement your own custom screen-reader alerts, roles and instructions, you can use the `autoAriaDisabled` options and wire everything up yourself using markup and the `consider` and `finalize` handlers (for example: [unsortable list](https://svelte.dev/repl/e020ea1051dc4ae3ac2b697064f234bc?version=3.29.0)). | ||
_Note_: in general you probably want to use semantic-html (ex: `ol` and `li` elements rather than `section` and `div`) but the library is screen readers friendly regardless (or at least that's the goal :)). | ||
If you want to implement your own custom screen-reader alerts, roles and instructions, you can use the `autoAriaDisabled` options and wire everything up yourself using markup and the `consider` and `finalize` handlers (for example: [unsortable list](https://svelte.dev/repl/e020ea1051dc4ae3ac2b697064f234bc?version=3.29.0)). | ||
##### Keyboard support | ||
- Tab into a dnd container to get a description and instructions | ||
- Tab into an item and press the *Space*/*Enter* key to enter dragging-mode. The reader will tell the user a drag has started. | ||
- Use the *arrow keys* while in dragging-mode to change the item's position in the list (down and right are the same, up and left are the same). The reader will tell the user about position changes. | ||
- Tab to another dnd container while in dragging-mode in order to move the item to it (the item will be moved to it when it gets focus). The reader will tell the user that item was added to the new list. | ||
- Press *Space*/*Enter* key while focused on an item, or the *Escape* key anywhere to exit dragging mode. The reader will tell the user that they are no longer dragging. | ||
- Clicking on another item while in drag mode will make it the new drag target. Clicking outside of any draggable will exit dragging-mode (and tell the user) | ||
- Mouse drag and drop can be preformed independently of keyboard dragging (as in an item can be dragged with the mouse while in or out of keyboard initiated dragging-mode) | ||
- Keyboard drag uses the same `consider` (only on drag start) and `finalize` (every time the item is moved) events but share only some of the `TRIGGERS`. The same handlers should work fine for both. | ||
- Tab into a dnd container to get a description and instructions | ||
- Tab into an item and press the _Space_/_Enter_ key to enter dragging-mode. The reader will tell the user a drag has started. | ||
- Use the _arrow keys_ while in dragging-mode to change the item's position in the list (down and right are the same, up and left are the same). The reader will tell the user about position changes. | ||
- Tab to another dnd container while in dragging-mode in order to move the item to it (the item will be moved to it when it gets focus). The reader will tell the user that item was added to the new list. | ||
- Press _Space_/_Enter_ key while focused on an item, or the _Escape_ key anywhere to exit dragging mode. The reader will tell the user that they are no longer dragging. | ||
- Clicking on another item while in drag mode will make it the new drag target. Clicking outside of any draggable will exit dragging-mode (and tell the user) | ||
- Mouse drag and drop can be preformed independently of keyboard dragging (as in an item can be dragged with the mouse while in or out of keyboard initiated dragging-mode) | ||
- Keyboard drag uses the same `consider` (only on drag start) and `finalize` (every time the item is moved) events but share only some of the `TRIGGERS`. The same handlers should work fine for both. | ||
### Examples and Recipes | ||
* [Super basic, single list, no animation](https://svelte.dev/repl/bbd709b1a00b453e94658392c97a018a?version=3.24.1) | ||
* [Super basic, single list, with animation](https://svelte.dev/repl/3d544791e5c24fd4aa1eb983d749f776?version=3.24.1) | ||
* [Multiple dndzones, multiple types](https://svelte.dev/repl/4d23eb3b9e184b90b58f0867010ad258?version=3.24.1) | ||
* [Board (nested zones and multiple types), scrolling containers, scrolling page](https://svelte.dev/repl/e2ef044af75c4b16b424b8219fb31fd9?version=3.22.2) | ||
* [Selectively enable/disable drag/drop](https://svelte.dev/repl/44c9229556f3456e9883c10fc0aa0ee9?version=3) | ||
* [Custom active dropzone styling](https://svelte.dev/repl/4ceecc5bae54490b811bd62d4d613e59?version=3.24.1) | ||
* [Customizing the dragged element](https://svelte.dev/repl/438fca28bb1f4eb1b34eff9dc6a728dc?version=3) | ||
* [Customizing the placeholder(shadow) element](https://svelte.dev/repl/9c8db8b91b2142d19dcf9bc963a27838?version=3) | ||
* [Copy on drag, simple and Dragula like](https://svelte.dev/repl/924b4cc920524065a637fa910fe10193?version=3.24.1) | ||
* [Drag handles](https://svelte.dev/repl/4949485c5a8f46e7bdbeb73ed565a9c7?version=3.24.1), courtesy of @gleuch | ||
* [Unsortable lists with custom aria instructions](https://svelte.dev/repl/e020ea1051dc4ae3ac2b697064f234bc?version=3.29.0) | ||
* [Crazy nesting](https://svelte.dev/repl/fe8c9eca04f9417a94a8b6041df77139?version=3), courtesy of @zahachtah | ||
- [Super basic, single list, no animation](https://svelte.dev/repl/bbd709b1a00b453e94658392c97a018a?version=3.24.1) | ||
- [Super basic, single list, with animation](https://svelte.dev/repl/3d544791e5c24fd4aa1eb983d749f776?version=3.24.1) | ||
- [Multiple dndzones, multiple types](https://svelte.dev/repl/4d23eb3b9e184b90b58f0867010ad258?version=3.24.1) | ||
- [Board (nested zones and multiple types), scrolling containers, scrolling page](https://svelte.dev/repl/e2ef044af75c4b16b424b8219fb31fd9?version=3.22.2) | ||
- [Selectively enable/disable drag/drop](https://svelte.dev/repl/44c9229556f3456e9883c10fc0aa0ee9?version=3) | ||
- [Custom active dropzone styling](https://svelte.dev/repl/4ceecc5bae54490b811bd62d4d613e59?version=3.24.1) | ||
- [Customizing the dragged element](https://svelte.dev/repl/438fca28bb1f4eb1b34eff9dc6a728dc?version=3) | ||
- [Customizing the placeholder(shadow) element](https://svelte.dev/repl/9c8db8b91b2142d19dcf9bc963a27838?version=3) | ||
* [Fade in/out but without using Svelte transitions](https://svelte.dev/repl/3f1e68203ef140969a8240eba3475a8d?version=3.24.1) | ||
* [Nested fade in/out without using Svelte transitions](https://svelte.dev/repl/49b09aedfe0543b4bc8f575c8dbf9a53?version=3.24.1) | ||
- [Copy on drag, simple and Dragula like](https://svelte.dev/repl/924b4cc920524065a637fa910fe10193?version=3.24.1) | ||
- [Drag handles](https://svelte.dev/repl/4949485c5a8f46e7bdbeb73ed565a9c7?version=3.24.1), courtesy of @gleuch | ||
- [Unsortable lists with custom aria instructions](https://svelte.dev/repl/e020ea1051dc4ae3ac2b697064f234bc?version=3.29.0) | ||
- [Crazy nesting](https://svelte.dev/repl/fe8c9eca04f9417a94a8b6041df77139?version=3), courtesy of @zahachtah | ||
- [Fade in/out but without using Svelte transitions](https://svelte.dev/repl/3f1e68203ef140969a8240eba3475a8d?version=3.24.1) | ||
- [Nested fade in/out without using Svelte transitions](https://svelte.dev/repl/49b09aedfe0543b4bc8f575c8dbf9a53?version=3.24.1) | ||
### Rules/ assumptions to keep in mind | ||
* Only one element can be dragged in any given time | ||
* The data that represents items within dnd-zones **of the same type** is expected to have the same shape (as in a data object that represents an item in one container can be added to another without conversion). | ||
* Item ids (#each keys) are unique in all dnd containers of the same type. EVERY DRAGGABLE ITEM (passed in through `items`) MUST HAVE AN ID PROPERTY CALLED `id`. You can override it globally if you'd like to use a different key (see below) | ||
* The items in the list that is passed-in are in the same order as the children of the container (i.e the items are rendered in an #each block). | ||
* The host component should refresh the items that are passed in to the custom-action when receiving consider and finalize events. | ||
* FYI, the library assumes it is okay to add a temporary item to the items list in any of the dnd-zones while an element is dragged around. | ||
* If you want dragged items to be able to scroll the container, make sure the scroll-container (the element with overflow:scroll) is the dnd-zone (the element decorated with this custom action) | ||
* Svelte's built-in transitions might not play nice with this library. Luckily, it is an easy issue to work around. There are examples below. | ||
- Only one element can be dragged in any given time | ||
- The data that represents items within dnd-zones **of the same type** is expected to have the same shape (as in a data object that represents an item in one container can be added to another without conversion). | ||
- Item ids (#each keys) are unique in all dnd containers of the same type. EVERY DRAGGABLE ITEM (passed in through `items`) MUST HAVE AN ID PROPERTY CALLED `id`. You can override it globally if you'd like to use a different key (see below) | ||
- The items in the list that is passed-in are in the same order as the children of the container (i.e the items are rendered in an #each block). | ||
- The host component should refresh the items that are passed in to the custom-action when receiving consider and finalize events. | ||
- FYI, the library assumes it is okay to add a temporary item to the items list in any of the dnd-zones while an element is dragged around. | ||
- If you want dragged items to be able to scroll the container, make sure the scroll-container (the element with overflow:scroll) is the dnd-zone (the element decorated with this custom action) | ||
- Svelte's built-in transitions might not play nice with this library. Luckily, it is an easy issue to work around. There are examples below. | ||
### Overriding the item id key name | ||
Sometimes it is useful to use a different key for your items instead of `id`, for example when working with PouchDB which expects `_id`. It can save some annoying conversions back and forth. | ||
In such cases you can import and call `overrideItemIdKeyNameBeforeInitialisingDndZones`. This function accepts one parameter of type `string` which is the new id key name. | ||
For example: | ||
```javascript | ||
import {overrideItemIdKeyNameBeforeInitialisingDndZones} from 'svelte-dnd-action'; | ||
overrideItemIdKeyNameBeforeInitialisingDndZones('_id'); | ||
``` | ||
import {overrideItemIdKeyNameBeforeInitialisingDndZones} from "svelte-dnd-action"; | ||
overrideItemIdKeyNameBeforeInitialisingDndZones("_id"); | ||
``` | ||
It applies globally (as in, all of your items everywhere are expected to have a unique identifier with this name). It can only be called when there are no rendered dndzones (I recommend calling it within the top-level <script> tag, ex: in the App component). | ||
### Debug output | ||
By default no debug output will be logged to the console. If you want to see internal debug messages, you can enable the debug output like this: | ||
```javascript | ||
import {setDebug} from 'svelte-dnd-action'; | ||
import {setDebug} from "svelte-dnd-action"; | ||
setDebug(true); | ||
``` | ||
``` | ||
### Typescript | ||
If you are using Typescript, you will need to add the following block to your `global.d.ts` (at least until [this svelte issue](https://github.com/sveltejs/language-tools/issues/431) is resolved): | ||
```typescript | ||
declare type DndEvent = import('svelte-dnd-action').DndEvent; | ||
declare type DndEvent = import("svelte-dnd-action").DndEvent; | ||
declare namespace svelte.JSX { | ||
interface HTMLAttributes<T> { | ||
onconsider?: (event: CustomEvent<DndEvent> & { target: EventTarget & T }) => void; | ||
onfinalize?: (event: CustomeEvent<DndEvent> & { target: EventTarget & T }) => void; | ||
onconsider?: (event: CustomEvent<DndEvent> & {target: EventTarget & T}) => void; | ||
onfinalize?: (event: CustomeEvent<DndEvent> & {target: EventTarget & T}) => void; | ||
} | ||
} | ||
``` | ||
You may need to edit `tsconfig.json` to include `global.d.ts` if it doesn't already: "include": ["src/**/*", "global.d.ts"]. | ||
Then you will be able to use the library with type safety as follows (Typescript gurus out there, improvements are welcome :smile:): | ||
```html | ||
<style> | ||
section { | ||
width: 12em; | ||
padding: 1em; | ||
height: 7.5em; | ||
} | ||
div { | ||
height: 1.5em; | ||
width: 10em; | ||
text-align: center; | ||
border: 1px solid black; | ||
margin: 0.2em; | ||
padding: 0.3em; | ||
} | ||
section { | ||
width: 12em; | ||
padding: 1em; | ||
height: 7.5em; | ||
} | ||
div { | ||
height: 1.5em; | ||
width: 10em; | ||
text-align: center; | ||
border: 1px solid black; | ||
margin: 0.2em; | ||
padding: 0.3em; | ||
} | ||
</style> | ||
<script lang="ts"> | ||
import {dndzone} from 'svelte-dnd-action'; | ||
import {flip} from 'svelte/animate'; | ||
const flipDurationMs = 200; | ||
function handleSort(e: CustomEvent<DndEvent>) { | ||
items = e.detail.items as {id: number, title:string}[]; | ||
} | ||
let items = [ | ||
{id:1, title: 'I'}, | ||
{id:2, title: 'Am'}, | ||
{id:3, title: 'Yoda'} | ||
]; | ||
import {dndzone} from "svelte-dnd-action"; | ||
import {flip} from "svelte/animate"; | ||
const flipDurationMs = 200; | ||
function handleSort(e: CustomEvent<DndEvent>) { | ||
items = e.detail.items as {id: number; title: string}[]; | ||
} | ||
let items = [ | ||
{id: 1, title: "I"}, | ||
{id: 2, title: "Am"}, | ||
{id: 3, title: "Yoda"} | ||
]; | ||
</script> | ||
<section use:dndzone={{items, flipDurationMs}} on:consider={handleSort} on:finalize={handleSort}> | ||
{#each items as item(item.id)} | ||
<div animate:flip={{duration:flipDurationMs}}> | ||
{item.title} | ||
</div> | ||
{/each} | ||
<section use:dndzone="{{items," flipDurationMs}} on:consider="{handleSort}" on:finalize="{handleSort}"> | ||
{#each items as item(item.id)} | ||
<div animate:flip="{{duration:flipDurationMs}}">{item.title}</div> | ||
{/each} | ||
</section> | ||
@@ -254,4 +273,5 @@ ``` | ||
### Contributing [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/isaacHagoel/svelte-dnd-action/issues) | ||
There is still quite a lot to do. If you'd like to contribute please get in touch (raise an issue or comment on an existing one). | ||
Ideally, be specific about which area you'd like to help with. | ||
Thank you for reading :) |
@@ -38,3 +38,3 @@ import {dndzone as pointerDndZone} from "./pointerAction"; | ||
} | ||
} | ||
}; | ||
} | ||
@@ -66,2 +66,2 @@ | ||
} | ||
} | ||
} |
@@ -18,9 +18,11 @@ import {DRAGGED_ENTERED_EVENT_NAME, DRAGGED_LEFT_EVENT_NAME, DRAGGED_OVER_INDEX_EVENT_NAME} from "./helpers/dispatcher"; | ||
export const SHADOW_ITEM_MARKER_PROPERTY_NAME = 'isDndShadowItem'; | ||
export const SHADOW_ITEM_MARKER_PROPERTY_NAME = "isDndShadowItem"; | ||
export let ITEM_ID_KEY = "id"; | ||
let activeDndZoneCount = 0; | ||
export function incrementActiveDropZoneCount() {activeDndZoneCount++;} | ||
export function incrementActiveDropZoneCount() { | ||
activeDndZoneCount++; | ||
} | ||
export function decrementActiveDropZoneCount() { | ||
if (activeDndZoneCount === 0) { | ||
throw new Error("Bug! trying to decrement when there are no dropzones") | ||
throw new Error("Bug! trying to decrement when there are no dropzones"); | ||
} | ||
@@ -43,7 +45,7 @@ activeDndZoneCount--; | ||
} | ||
printDebug(() => ["overriding item id key name", newKeyName]) | ||
printDebug(() => ["overriding item id key name", newKeyName]); | ||
ITEM_ID_KEY = newKeyName; | ||
} | ||
export const isOnServer = (typeof window === 'undefined'); | ||
export const isOnServer = typeof window === "undefined"; | ||
@@ -61,11 +63,10 @@ export let printDebug = () => {}; | ||
if (Array.isArray(message)) { | ||
logFunction(...message) | ||
logFunction(...message); | ||
} else { | ||
logFunction(message) | ||
logFunction(message); | ||
} | ||
} | ||
} | ||
else { | ||
}; | ||
} else { | ||
printDebug = () => {}; | ||
} | ||
} | ||
} |
import {isOnServer} from "../constants"; | ||
const INSTRUCTION_IDs = { | ||
DND_ZONE_ACTIVE: 'dnd-zone-active', | ||
DND_ZONE_DRAG_DISABLED: 'dnd-zone-drag-disabled' | ||
} | ||
DND_ZONE_ACTIVE: "dnd-zone-active", | ||
DND_ZONE_DRAG_DISABLED: "dnd-zone-drag-disabled" | ||
}; | ||
const ID_TO_INSTRUCTION = { | ||
[INSTRUCTION_IDs.DND_ZONE_ACTIVE]: "Tab to one the items and press space-bar or enter to start dragging it", | ||
[INSTRUCTION_IDs.DND_ZONE_DRAG_DISABLED]: "This is a disabled drag and drop list" | ||
} | ||
}; | ||
const ALERT_DIV_ID = 'dnd-action-aria-alert'; | ||
const ALERT_DIV_ID = "dnd-action-aria-alert"; | ||
let alertsDiv; | ||
@@ -22,3 +22,3 @@ /** | ||
// setting the dynamic alerts | ||
alertsDiv = document.createElement('div'); | ||
alertsDiv = document.createElement("div"); | ||
(function initAlertsDiv() { | ||
@@ -28,9 +28,9 @@ alertsDiv.id = ALERT_DIV_ID; | ||
//alertsDiv.tabIndex = -1; | ||
alertsDiv.style.position = 'fixed'; | ||
alertsDiv.style.bottom = '0'; | ||
alertsDiv.style.left = '0'; | ||
alertsDiv.style.zIndex = '-5'; | ||
alertsDiv.style.opacity = '0'; | ||
alertsDiv.style.height = '0'; | ||
alertsDiv.style.width = '0'; | ||
alertsDiv.style.position = "fixed"; | ||
alertsDiv.style.bottom = "0"; | ||
alertsDiv.style.left = "0"; | ||
alertsDiv.style.zIndex = "-5"; | ||
alertsDiv.style.opacity = "0"; | ||
alertsDiv.style.height = "0"; | ||
alertsDiv.style.width = "0"; | ||
alertsDiv.setAttribute("role", "alert"); | ||
@@ -41,12 +41,12 @@ })(); | ||
// setting the instructions | ||
Object.entries(ID_TO_INSTRUCTION).forEach(([id, txt]) => document.body.prepend(instructionToHiddenDiv(id, txt))); | ||
Object.entries(ID_TO_INSTRUCTION).forEach(([id, txt]) => document.body.prepend(instructionToHiddenDiv(id, txt))); | ||
return {...INSTRUCTION_IDs}; | ||
} | ||
function instructionToHiddenDiv(id, txt) { | ||
const div = document.createElement('div'); | ||
const div = document.createElement("div"); | ||
div.id = id; | ||
div.innerHTML = `<p>${txt}</p>`; | ||
div.style.display = 'none'; | ||
div.style.position = 'fixed'; | ||
div.style.zIndex = '-5'; | ||
div.style.display = "none"; | ||
div.style.position = "fixed"; | ||
div.style.zIndex = "-5"; | ||
return div; | ||
@@ -60,8 +60,8 @@ } | ||
export function alertToScreenReader(txt) { | ||
alertsDiv.innerHTML = ''; | ||
alertsDiv.innerHTML = ""; | ||
const alertText = document.createTextNode(txt); | ||
alertsDiv.appendChild(alertText); | ||
// this is needed for Safari | ||
alertsDiv.style.display = 'none'; | ||
alertsDiv.style.display = 'inline'; | ||
alertsDiv.style.display = "none"; | ||
alertsDiv.style.display = "inline"; | ||
} |
// external events | ||
const FINALIZE_EVENT_NAME = 'finalize'; | ||
const CONSIDER_EVENT_NAME = 'consider'; | ||
const FINALIZE_EVENT_NAME = "finalize"; | ||
const CONSIDER_EVENT_NAME = "consider"; | ||
@@ -15,5 +15,7 @@ /** | ||
export function dispatchFinalizeEvent(el, items, info) { | ||
el.dispatchEvent(new CustomEvent(FINALIZE_EVENT_NAME, { | ||
detail: {items, info} | ||
})); | ||
el.dispatchEvent( | ||
new CustomEvent(FINALIZE_EVENT_NAME, { | ||
detail: {items, info} | ||
}) | ||
); | ||
} | ||
@@ -28,31 +30,41 @@ | ||
export function dispatchConsiderEvent(el, items, info) { | ||
el.dispatchEvent(new CustomEvent(CONSIDER_EVENT_NAME, { | ||
detail: {items, info} | ||
})); | ||
el.dispatchEvent( | ||
new CustomEvent(CONSIDER_EVENT_NAME, { | ||
detail: {items, info} | ||
}) | ||
); | ||
} | ||
// internal events | ||
export const DRAGGED_ENTERED_EVENT_NAME = 'draggedEntered'; | ||
export const DRAGGED_LEFT_EVENT_NAME = 'draggedLeft'; | ||
export const DRAGGED_OVER_INDEX_EVENT_NAME = 'draggedOverIndex'; | ||
export const DRAGGED_LEFT_DOCUMENT_EVENT_NAME = 'draggedLeftDocument'; | ||
export const DRAGGED_ENTERED_EVENT_NAME = "draggedEntered"; | ||
export const DRAGGED_LEFT_EVENT_NAME = "draggedLeft"; | ||
export const DRAGGED_OVER_INDEX_EVENT_NAME = "draggedOverIndex"; | ||
export const DRAGGED_LEFT_DOCUMENT_EVENT_NAME = "draggedLeftDocument"; | ||
export function dispatchDraggedElementEnteredContainer(containerEl, indexObj, draggedEl) { | ||
containerEl.dispatchEvent(new CustomEvent(DRAGGED_ENTERED_EVENT_NAME, { | ||
detail: {indexObj, draggedEl} | ||
})); | ||
containerEl.dispatchEvent( | ||
new CustomEvent(DRAGGED_ENTERED_EVENT_NAME, { | ||
detail: {indexObj, draggedEl} | ||
}) | ||
); | ||
} | ||
export function dispatchDraggedElementLeftContainer(containerEl, draggedEl) { | ||
containerEl.dispatchEvent(new CustomEvent(DRAGGED_LEFT_EVENT_NAME, { | ||
detail: {draggedEl} | ||
})); | ||
containerEl.dispatchEvent( | ||
new CustomEvent(DRAGGED_LEFT_EVENT_NAME, { | ||
detail: {draggedEl} | ||
}) | ||
); | ||
} | ||
export function dispatchDraggedElementIsOverIndex(containerEl, indexObj, draggedEl) { | ||
containerEl.dispatchEvent(new CustomEvent(DRAGGED_OVER_INDEX_EVENT_NAME, { | ||
detail: {indexObj, draggedEl} | ||
})); | ||
containerEl.dispatchEvent( | ||
new CustomEvent(DRAGGED_OVER_INDEX_EVENT_NAME, { | ||
detail: {indexObj, draggedEl} | ||
}) | ||
); | ||
} | ||
export function dispatchDraggedLeftDocument(draggedEl) { | ||
window.dispatchEvent(new CustomEvent(DRAGGED_LEFT_DOCUMENT_EVENT_NAME, { | ||
detail: { draggedEl} | ||
})); | ||
window.dispatchEvent( | ||
new CustomEvent(DRAGGED_LEFT_DOCUMENT_EVENT_NAME, { | ||
detail: {draggedEl} | ||
}) | ||
); | ||
} |
@@ -11,4 +11,4 @@ // This is based off https://stackoverflow.com/questions/27745438/how-to-compute-getboundingclientrect-without-considering-transforms/57876601#57876601 | ||
let sx, sy, dx, dy; | ||
if (tx.startsWith('matrix3d(')) { | ||
ta = tx.slice(9,-1).split(/, /); | ||
if (tx.startsWith("matrix3d(")) { | ||
ta = tx.slice(9, -1).split(/, /); | ||
sx = +ta[0]; | ||
@@ -18,3 +18,3 @@ sy = +ta[5]; | ||
dy = +ta[13]; | ||
} else if (tx.startsWith('matrix(')) { | ||
} else if (tx.startsWith("matrix(")) { | ||
ta = tx.slice(7, -1).split(/, /); | ||
@@ -31,7 +31,14 @@ sx = +ta[0]; | ||
const x = rect.x - dx - (1 - sx) * parseFloat(to); | ||
const y = rect.y - dy - (1 - sy) * parseFloat(to.slice(to.indexOf(' ') + 1)); | ||
const y = rect.y - dy - (1 - sy) * parseFloat(to.slice(to.indexOf(" ") + 1)); | ||
const w = sx ? rect.width / sx : el.offsetWidth; | ||
const h = sy ? rect.height / sy : el.offsetHeight; | ||
return { | ||
x: x, y: y, width: w, height: h, top: y, right: x + w, bottom: y + h, left: x | ||
x: x, | ||
y: y, | ||
width: w, | ||
height: h, | ||
top: y, | ||
right: x + w, | ||
bottom: y + h, | ||
left: x | ||
}; | ||
@@ -50,3 +57,3 @@ } else { | ||
const rect = adjustedBoundingRect(el); | ||
return ({ | ||
return { | ||
top: rect.top + window.scrollY, | ||
@@ -56,3 +63,3 @@ bottom: rect.bottom + window.scrollY, | ||
right: rect.right + window.scrollX | ||
}); | ||
}; | ||
} | ||
@@ -67,3 +74,3 @@ | ||
const rect = el.getBoundingClientRect(); | ||
return ({ | ||
return { | ||
top: rect.top + window.scrollY, | ||
@@ -73,3 +80,3 @@ bottom: rect.bottom + window.scrollY, | ||
right: rect.right + window.scrollX | ||
}); | ||
}; | ||
} | ||
@@ -88,6 +95,6 @@ | ||
export function findCenter(rect) { | ||
return ({ | ||
x: (rect.left + rect.right) /2, | ||
y: (rect.top + rect.bottom) /2 | ||
}); | ||
return { | ||
x: (rect.left + rect.right) / 2, | ||
y: (rect.top + rect.bottom) / 2 | ||
}; | ||
} | ||
@@ -104,3 +111,3 @@ | ||
function calcDistance(pointA, pointB) { | ||
return Math.sqrt(Math.pow(pointA.x - pointB.x, 2) + Math.pow(pointA.y - pointB.y, 2)); | ||
return Math.sqrt(Math.pow(pointA.x - pointB.x, 2) + Math.pow(pointA.y - pointB.y, 2)); | ||
} | ||
@@ -114,7 +121,3 @@ | ||
function isPointInsideRect(point, rect) { | ||
return ( | ||
(point.y <= rect.bottom && point.y >= rect.top) | ||
&& | ||
(point.x >= rect.left && point.x <= rect.right) | ||
); | ||
return point.y <= rect.bottom && point.y >= rect.top && point.x >= rect.left && point.x <= rect.right; | ||
} | ||
@@ -128,3 +131,3 @@ | ||
export function findCenterOfElement(el) { | ||
return findCenter( getAbsoluteRect(el)); | ||
return findCenter(getAbsoluteRect(el)); | ||
} | ||
@@ -180,3 +183,3 @@ | ||
right: Math.min(rect.right, document.documentElement.clientWidth) - point.x | ||
} | ||
} | ||
}; | ||
} |
@@ -1,2 +0,2 @@ | ||
import { isCenterOfAInsideB, calcDistanceBetweenCenters } from './intersection'; | ||
import {isCenterOfAInsideB, calcDistanceBetweenCenters} from "./intersection"; | ||
@@ -10,4 +10,4 @@ /** | ||
* Find the index for the dragged element in the list it is dragged over | ||
* @param {HTMLElement} floatingAboveEl | ||
* @param {HTMLElement} collectionBelowEl | ||
* @param {HTMLElement} floatingAboveEl | ||
* @param {HTMLElement} collectionBelowEl | ||
* @returns {Index|null} - if the element is over the container the Index object otherwise null | ||
@@ -20,3 +20,3 @@ */ | ||
const children = collectionBelowEl.children; | ||
// the container is empty, floating element should be the first | ||
// the container is empty, floating element should be the first | ||
if (children.length === 0) { | ||
@@ -27,3 +27,3 @@ return {index: 0, isProximityBased: true}; | ||
// a possible improvement: pass in the lastIndex it was found in and check there first, then expand from there | ||
for (let i=0; i< children.length; i++) { | ||
for (let i = 0; i < children.length; i++) { | ||
if (isCenterOfAInsideB(floatingAboveEl, children[i])) { | ||
@@ -33,3 +33,3 @@ return {index: i, isProximityBased: false}; | ||
} | ||
// this can happen if there is space around the children so the floating element has | ||
// this can happen if there is space around the children so the floating element has | ||
//entered the container but not any of the children, in this case we will find the nearest child | ||
@@ -39,3 +39,3 @@ let minDistanceSoFar = Number.MAX_VALUE; | ||
// we are checking all of them because we don't know whether we are dealing with a horizontal or vertical container and where the floating element entered from | ||
for (let i=0; i< children.length; i++) { | ||
for (let i = 0; i < children.length; i++) { | ||
const distance = calcDistanceBetweenCenters(floatingAboveEl, children[i]); | ||
@@ -48,2 +48,2 @@ if (distance < minDistanceSoFar) { | ||
return {index: indexOfMin, isProximityBased: true}; | ||
} | ||
} |
@@ -1,10 +0,11 @@ | ||
import {findWouldBeIndex} from './listUtil'; | ||
import {findWouldBeIndex} from "./listUtil"; | ||
import {findCenterOfElement, isElementOffDocument} from "./intersection"; | ||
import {dispatchDraggedElementEnteredContainer, | ||
dispatchDraggedElementLeftContainer, | ||
dispatchDraggedLeftDocument, | ||
dispatchDraggedElementIsOverIndex} | ||
from './dispatcher'; | ||
import { | ||
dispatchDraggedElementEnteredContainer, | ||
dispatchDraggedElementLeftContainer, | ||
dispatchDraggedLeftDocument, | ||
dispatchDraggedElementIsOverIndex | ||
} from "./dispatcher"; | ||
import {makeScroller} from "./scroller"; | ||
import { getDepth } from "./util"; | ||
import {getDepth} from "./util"; | ||
import {printDebug} from "../constants"; | ||
@@ -17,7 +18,6 @@ | ||
/** | ||
* Tracks the dragged elements and performs the side effects when it is dragged over a drop zone (basically dispatching custom-events scrolling) | ||
* @param {Set<HTMLElement>} dropZones | ||
* @param {HTMLElement} draggedEl | ||
* @param {Set<HTMLElement>} dropZones | ||
* @param {HTMLElement} draggedEl | ||
* @param {number} [intervalMs = INTERVAL_MS] | ||
@@ -41,5 +41,8 @@ */ | ||
// we only want to make a new decision after the element was moved a bit to prevent flickering | ||
if (!scrolled && lastCentrePositionOfDragged && | ||
if ( | ||
!scrolled && | ||
lastCentrePositionOfDragged && | ||
Math.abs(lastCentrePositionOfDragged.x - currentCenterOfDragged.x) < TOLERANCE_PX && | ||
Math.abs(lastCentrePositionOfDragged.y - currentCenterOfDragged.y) < TOLERANCE_PX ) { | ||
Math.abs(lastCentrePositionOfDragged.y - currentCenterOfDragged.y) < TOLERANCE_PX | ||
) { | ||
next = window.setTimeout(andNow, intervalMs); | ||
@@ -56,8 +59,8 @@ return; | ||
// this is a simple algorithm, potential improvement: first look at lastDropZoneFound | ||
let isDraggedInADropZone = false | ||
let isDraggedInADropZone = false; | ||
for (const dz of dropZonesFromDeepToShallow) { | ||
const indexObj = findWouldBeIndex(draggedEl, dz); | ||
if (indexObj === null) { | ||
// it is not inside | ||
continue; | ||
// it is not inside | ||
continue; | ||
} | ||
@@ -72,4 +75,3 @@ const {index} = indexObj; | ||
lastIndexFound = index; | ||
} | ||
else if (index !== lastIndexFound) { | ||
} else if (index !== lastIndexFound) { | ||
dispatchDraggedElementIsOverIndex(dz, indexObj, draggedEl); | ||
@@ -100,2 +102,2 @@ lastIndexFound = index; | ||
resetScrolling(); | ||
} | ||
} |
@@ -1,4 +0,2 @@ | ||
import { | ||
calcInnerDistancesBetweenPointAndSidesOfElement, | ||
} from "./intersection"; | ||
import {calcInnerDistancesBetweenPointAndSidesOfElement} from "./intersection"; | ||
const SCROLL_ZONE_PX = 25; | ||
@@ -15,3 +13,3 @@ | ||
const {directionObj, stepPx} = scrollingInfo; | ||
if(directionObj) { | ||
if (directionObj) { | ||
containerEl.scrollBy(directionObj.x * stepPx, directionObj.y * stepPx); | ||
@@ -45,7 +43,7 @@ window.requestAnimationFrame(() => scrollContainer(containerEl)); | ||
scrollingVertically = true; | ||
scrollingInfo.directionObj = {x:0, y:1}; | ||
scrollingInfo.directionObj = {x: 0, y: 1}; | ||
scrollingInfo.stepPx = calcScrollStepPx(distances.bottom); | ||
} else if (distances.top < SCROLL_ZONE_PX) { | ||
scrollingVertically = true; | ||
scrollingInfo.directionObj = {x:0, y:-1}; | ||
scrollingInfo.directionObj = {x: 0, y: -1}; | ||
scrollingInfo.stepPx = calcScrollStepPx(distances.top); | ||
@@ -62,10 +60,10 @@ } | ||
scrollingHorizontally = true; | ||
scrollingInfo.directionObj = {x:1, y:0}; | ||
scrollingInfo.directionObj = {x: 1, y: 0}; | ||
scrollingInfo.stepPx = calcScrollStepPx(distances.right); | ||
} else if (distances.left < SCROLL_ZONE_PX) { | ||
scrollingHorizontally = true; | ||
scrollingInfo.directionObj = {x:-1, y:0}; | ||
scrollingInfo.directionObj = {x: -1, y: 0}; | ||
scrollingInfo.stepPx = calcScrollStepPx(distances.left); | ||
} | ||
if (!isAlreadyScrolling && scrollingHorizontally){ | ||
if (!isAlreadyScrolling && scrollingHorizontally) { | ||
scrollContainer(elementToScroll); | ||
@@ -79,6 +77,6 @@ return true; | ||
return ({ | ||
return { | ||
scrollIfNeeded, | ||
resetScrolling | ||
}); | ||
} | ||
}; | ||
} |
@@ -24,12 +24,12 @@ const TRANSITION_DURATION_SECONDS = 0.2; | ||
draggedEl.style.left = `${rect.left}px`; | ||
draggedEl.style.margin = '0'; | ||
draggedEl.style.margin = "0"; | ||
// we can't have relative or automatic height and width or it will break the illusion | ||
draggedEl.style.boxSizing = 'border-box'; | ||
draggedEl.style.boxSizing = "border-box"; | ||
draggedEl.style.height = `${rect.height}px`; | ||
draggedEl.style.width = `${rect.width}px`; | ||
draggedEl.style.transition = `${trs('width')}, ${trs('height')}, ${trs('background-color')}, ${trs('opacity')}, ${trs('color')} `; | ||
draggedEl.style.transition = `${trs("width")}, ${trs("height")}, ${trs("background-color")}, ${trs("opacity")}, ${trs("color")} `; | ||
// this is a workaround for a strange browser bug that causes the right border to disappear when all the transitions are added at the same time | ||
window.setTimeout(() => draggedEl.style.transition +=`, ${trs('top')}, ${trs('left')}`,0); | ||
draggedEl.style.zIndex = '9999'; | ||
draggedEl.style.cursor = 'grabbing'; | ||
window.setTimeout(() => (draggedEl.style.transition += `, ${trs("top")}, ${trs("left")}`), 0); | ||
draggedEl.style.zIndex = "9999"; | ||
draggedEl.style.cursor = "grabbing"; | ||
@@ -44,3 +44,3 @@ return draggedEl; | ||
export function moveDraggedElementToWasDroppedState(draggedEl) { | ||
draggedEl.style.cursor = 'grab'; | ||
draggedEl.style.cursor = "grab"; | ||
} | ||
@@ -85,7 +85,18 @@ | ||
Array.from(computedStyle) | ||
.filter(s => s.startsWith('background') || s.startsWith('padding') || s.startsWith('font') || s.startsWith('text') || s.startsWith('align') || | ||
s.startsWith('justify') || s.startsWith('display') || s.startsWith('flex') || s.startsWith('border') || s === 'opacity' || s === 'color' || s === 'list-style-type') | ||
.forEach(s => | ||
copyToEl.style.setProperty(s, computedStyle.getPropertyValue(s), computedStyle.getPropertyPriority(s)) | ||
); | ||
.filter( | ||
s => | ||
s.startsWith("background") || | ||
s.startsWith("padding") || | ||
s.startsWith("font") || | ||
s.startsWith("text") || | ||
s.startsWith("align") || | ||
s.startsWith("justify") || | ||
s.startsWith("display") || | ||
s.startsWith("flex") || | ||
s.startsWith("border") || | ||
s === "opacity" || | ||
s === "color" || | ||
s === "list-style-type" | ||
) | ||
.forEach(s => copyToEl.style.setProperty(s, computedStyle.getPropertyValue(s), computedStyle.getPropertyPriority(s))); | ||
} | ||
@@ -102,9 +113,8 @@ | ||
if (!dragDisabled) { | ||
draggableEl.style.userSelect = 'none'; | ||
draggableEl.style.cursor = 'grab'; | ||
draggableEl.style.userSelect = "none"; | ||
draggableEl.style.cursor = "grab"; | ||
} else { | ||
draggableEl.style.userSelect = ""; | ||
draggableEl.style.cursor = ""; | ||
} | ||
else { | ||
draggableEl.style.userSelect = ''; | ||
draggableEl.style.cursor = ''; | ||
} | ||
} | ||
@@ -117,5 +127,5 @@ | ||
export function hideOriginalDragTarget(dragTarget) { | ||
dragTarget.style.display = 'none'; | ||
dragTarget.style.position = 'fixed'; | ||
dragTarget.style.zIndex = '-5'; | ||
dragTarget.style.display = "none"; | ||
dragTarget.style.position = "fixed"; | ||
dragTarget.style.zIndex = "-5"; | ||
} | ||
@@ -138,3 +148,3 @@ | ||
dropZones.forEach(dz => { | ||
const styles = getStyles(dz) | ||
const styles = getStyles(dz); | ||
Object.keys(styles).forEach(style => { | ||
@@ -153,7 +163,7 @@ dz.style[style] = styles[style]; | ||
dropZones.forEach(dz => { | ||
const styles = getStyles(dz) | ||
const styles = getStyles(dz); | ||
Object.keys(styles).forEach(style => { | ||
dz.style[style] = ''; | ||
dz.style[style] = ""; | ||
}); | ||
}); | ||
} |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
export function getDepth(node){ | ||
export function getDepth(node) { | ||
if (!node) { | ||
@@ -39,3 +39,3 @@ throw new Error("cannot get depth of a falsy node"); | ||
for (const keyA in objA) { | ||
if(!{}.hasOwnProperty.call(objB, keyA) || objB[keyA] !== objA[keyA]) { | ||
if (!{}.hasOwnProperty.call(objB, keyA) || objB[keyA] !== objA[keyA]) { | ||
return false; | ||
@@ -45,2 +45,2 @@ } | ||
return true; | ||
} | ||
} |
@@ -13,3 +13,3 @@ import {makeScroller} from "./scroller"; | ||
export function updateMousePosition(e) { | ||
const c = e.touches? e.touches[0] : e; | ||
const c = e.touches ? e.touches[0] : e; | ||
mousePosition = {x: c.clientX, y: c.clientY}; | ||
@@ -31,5 +31,5 @@ } | ||
export function armWindowScroller() { | ||
printDebug(() => 'arming window scroller'); | ||
window.addEventListener('mousemove', updateMousePosition); | ||
window.addEventListener('touchmove', updateMousePosition); | ||
printDebug(() => "arming window scroller"); | ||
window.addEventListener("mousemove", updateMousePosition); | ||
window.addEventListener("touchmove", updateMousePosition); | ||
loop(); | ||
@@ -42,8 +42,8 @@ } | ||
export function disarmWindowScroller() { | ||
printDebug(() => 'disarming window scroller'); | ||
window.removeEventListener('mousemove', updateMousePosition); | ||
window.removeEventListener('touchmove', updateMousePosition); | ||
printDebug(() => "disarming window scroller"); | ||
window.removeEventListener("mousemove", updateMousePosition); | ||
window.removeEventListener("touchmove", updateMousePosition); | ||
mousePosition = undefined; | ||
window.clearTimeout(next); | ||
resetScrolling(); | ||
} | ||
} |
@@ -1,3 +0,3 @@ | ||
export { dndzone } from './action.js'; | ||
export { alertToScreenReader } from './helpers/aria'; | ||
export { TRIGGERS, SOURCES, SHADOW_ITEM_MARKER_PROPERTY_NAME, overrideItemIdKeyNameBeforeInitialisingDndZones, setDebugMode } from './constants'; | ||
export {dndzone} from "./action.js"; | ||
export {alertToScreenReader} from "./helpers/aria"; | ||
export {TRIGGERS, SOURCES, SHADOW_ITEM_MARKER_PROPERTY_NAME, overrideItemIdKeyNameBeforeInitialisingDndZones, setDebugMode} from "./constants"; |
@@ -1,8 +0,2 @@ | ||
import { | ||
decrementActiveDropZoneCount, | ||
incrementActiveDropZoneCount, | ||
ITEM_ID_KEY, | ||
SOURCES, | ||
TRIGGERS | ||
} from "./constants"; | ||
import {decrementActiveDropZoneCount, incrementActiveDropZoneCount, ITEM_ID_KEY, SOURCES, TRIGGERS} from "./constants"; | ||
import {styleActiveDropZones, styleInactiveDropZones} from "./helpers/styler"; | ||
@@ -14,5 +8,5 @@ import {dispatchConsiderEvent, dispatchFinalizeEvent} from "./helpers/dispatcher"; | ||
const DEFAULT_DROP_ZONE_TYPE = '--any--'; | ||
const DEFAULT_DROP_ZONE_TYPE = "--any--"; | ||
const DEFAULT_DROP_TARGET_STYLE = { | ||
outline: 'rgba(255, 255, 102, 0.7) solid 2px', | ||
outline: "rgba(255, 255, 102, 0.7) solid 2px" | ||
}; | ||
@@ -35,4 +29,4 @@ | ||
/* TODO (potentially) | ||
* what's the deal with the black border of voice-reader not following focus? | ||
* maybe keep focus on the last dragged item upon drop? | ||
* what's the deal with the black border of voice-reader not following focus? | ||
* maybe keep focus on the last dragged item upon drop? | ||
*/ | ||
@@ -44,7 +38,7 @@ | ||
function registerDropZone(dropZoneEl, type) { | ||
printDebug(() => 'registering drop-zone if absent'); | ||
printDebug(() => "registering drop-zone if absent"); | ||
if (typeToDropZones.size === 0) { | ||
printDebug(() => "adding global keydown and click handlers"); | ||
window.addEventListener("keydown", globalKeyDownHandler); | ||
window.addEventListener('click', globalClickHandler); | ||
printDebug(() => "adding global keydown and click handlers"); | ||
window.addEventListener("keydown", globalKeyDownHandler); | ||
window.addEventListener("click", globalClickHandler); | ||
} | ||
@@ -60,3 +54,3 @@ if (!typeToDropZones.has(type)) { | ||
function unregisterDropZone(dropZoneEl, type) { | ||
printDebug(() => 'unregistering drop-zone'); | ||
printDebug(() => "unregistering drop-zone"); | ||
typeToDropZones.get(type).delete(dropZoneEl); | ||
@@ -70,3 +64,3 @@ decrementActiveDropZoneCount(); | ||
window.removeEventListener("keydown", globalKeyDownHandler); | ||
window.removeEventListener('click', globalClickHandler); | ||
window.removeEventListener("click", globalClickHandler); | ||
} | ||
@@ -77,4 +71,4 @@ } | ||
if (!isDragging) return; | ||
switch(e.key) { | ||
case("Escape"): { | ||
switch (e.key) { | ||
case "Escape": { | ||
handleDrop(); | ||
@@ -87,3 +81,3 @@ break; | ||
function globalClickHandler() { | ||
if (!isDragging) return ; | ||
if (!isDragging) return; | ||
if (!allDragTargets.has(document.activeElement)) { | ||
@@ -101,9 +95,12 @@ printDebug(() => "clicked outside of any draggable"); | ||
focusedDzLabel = newlyFocusedDz.getAttribute('aria-label') || ''; | ||
const {items:originItems} = dzToConfig.get(focusedDz); | ||
focusedDzLabel = newlyFocusedDz.getAttribute("aria-label") || ""; | ||
const {items: originItems} = dzToConfig.get(focusedDz); | ||
const originItem = originItems.find(item => item[ITEM_ID_KEY] === focusedItemId); | ||
const originIdx = originItems.indexOf(originItem); | ||
const itemToMove = originItems.splice(originIdx, 1)[0]; | ||
const {items:targetItems, autoAriaDisabled} = dzToConfig.get(newlyFocusedDz); | ||
if (newlyFocusedDz.getBoundingClientRect().top < focusedDz.getBoundingClientRect().top || newlyFocusedDz.getBoundingClientRect().left < focusedDz.getBoundingClientRect().left) { | ||
const {items: targetItems, autoAriaDisabled} = dzToConfig.get(newlyFocusedDz); | ||
if ( | ||
newlyFocusedDz.getBoundingClientRect().top < focusedDz.getBoundingClientRect().top || | ||
newlyFocusedDz.getBoundingClientRect().left < focusedDz.getBoundingClientRect().left | ||
) { | ||
targetItems.push(itemToMove); | ||
@@ -138,3 +135,7 @@ if (!autoAriaDisabled) { | ||
if (dispatchConsider) { | ||
dispatchConsiderEvent(focusedDz, dzToConfig.get(focusedDz).items, {trigger: TRIGGERS.DRAG_STOPPED, id: focusedItemId, source: SOURCES.KEYBOARD}); | ||
dispatchConsiderEvent(focusedDz, dzToConfig.get(focusedDz).items, { | ||
trigger: TRIGGERS.DRAG_STOPPED, | ||
id: focusedItemId, | ||
source: SOURCES.KEYBOARD | ||
}); | ||
} | ||
@@ -144,6 +145,6 @@ styleInactiveDropZones(typeToDropZones.get(draggedItemType), dz => dzToConfig.get(dz).dropTargetStyle); | ||
focusedItemId = null; | ||
focusedItemLabel = ''; | ||
focusedItemLabel = ""; | ||
draggedItemType = null; | ||
focusedDz = null; | ||
focusedDzLabel = ''; | ||
focusedDzLabel = ""; | ||
isDragging = false; | ||
@@ -154,3 +155,3 @@ triggerAllDzsUpdate(); | ||
export function dndzone(node, options) { | ||
const config = { | ||
const config = { | ||
items: undefined, | ||
@@ -164,3 +165,3 @@ type: undefined, | ||
function swap (arr, i, j) { | ||
function swap(arr, i, j) { | ||
if (arr.length <= 1) return; | ||
@@ -172,5 +173,5 @@ arr.splice(j, 1, arr.splice(i, 1, arr[j])[0]); | ||
printDebug(() => ["handling key down", e.key]); | ||
switch(e.key) { | ||
case("Enter"): | ||
case(" "): { | ||
switch (e.key) { | ||
case "Enter": | ||
case " ": { | ||
// we don't want to affect nested input elements | ||
@@ -187,8 +188,8 @@ if ((e.target.value !== undefined || e.target.isContentEditable) && !allDragTargets.has(e.target)) { | ||
// drag start | ||
handleDragStart(e) | ||
handleDragStart(e); | ||
} | ||
break; | ||
} | ||
case("ArrowDown"): | ||
case("ArrowRight"):{ | ||
case "ArrowDown": | ||
case "ArrowRight": { | ||
if (!isDragging) return; | ||
@@ -210,4 +211,4 @@ e.preventDefault(); // prevent scrolling | ||
} | ||
case("ArrowUp"): | ||
case("ArrowLeft"):{ | ||
case "ArrowUp": | ||
case "ArrowLeft": { | ||
if (!isDragging) return; | ||
@@ -234,3 +235,5 @@ e.preventDefault(); // prevent scrolling | ||
if (!config.autoAriaDisabled) { | ||
alertToScreenReader(`Started dragging item ${focusedItemLabel}. Use the arrow keys to move it within its list ${focusedDzLabel}, or tab to another list in order to move the item into it`); | ||
alertToScreenReader( | ||
`Started dragging item ${focusedItemLabel}. Use the arrow keys to move it within its list ${focusedDzLabel}, or tab to another list in order to move the item into it` | ||
); | ||
} | ||
@@ -242,5 +245,4 @@ setCurrentFocusedItem(e.currentTarget); | ||
styleActiveDropZones( | ||
Array.from(typeToDropZones.get(config.type)) | ||
.filter(dz => dz === focusedDz || !dzToConfig.get(dz).dropFromOthersDisabled), | ||
dz => dzToConfig.get(dz).dropTargetStyle, | ||
Array.from(typeToDropZones.get(config.type)).filter(dz => dz === focusedDz || !dzToConfig.get(dz).dropFromOthersDisabled), | ||
dz => dzToConfig.get(dz).dropTargetStyle | ||
); | ||
@@ -252,3 +254,3 @@ dispatchConsiderEvent(node, dzToConfig.get(node).items, {trigger: TRIGGERS.DRAG_STARTED, id: focusedItemId, source: SOURCES.KEYBOARD}); | ||
function handleClick(e) { | ||
if(!isDragging) return; | ||
if (!isDragging) return; | ||
if (e.currentTarget === focusedItem) return; | ||
@@ -265,13 +267,13 @@ e.stopPropagation(); | ||
focusedItemId = items[focusedItemIdx][ITEM_ID_KEY]; | ||
focusedItemLabel = children[focusedItemIdx].getAttribute('aria-label') || ''; | ||
focusedItemLabel = children[focusedItemIdx].getAttribute("aria-label") || ""; | ||
} | ||
function configure({ | ||
items = [], | ||
type: newType = DEFAULT_DROP_ZONE_TYPE, | ||
dragDisabled = false, | ||
dropFromOthersDisabled = false, | ||
dropTargetStyle = DEFAULT_DROP_TARGET_STYLE, | ||
autoAriaDisabled = false | ||
}) { | ||
items = [], | ||
type: newType = DEFAULT_DROP_ZONE_TYPE, | ||
dragDisabled = false, | ||
dropFromOthersDisabled = false, | ||
dropTargetStyle = DEFAULT_DROP_TARGET_STYLE, | ||
autoAriaDisabled = false | ||
}) { | ||
config.items = [...items]; | ||
@@ -285,3 +287,3 @@ config.dragDisabled = dragDisabled; | ||
node.setAttribute("role", "list"); | ||
node.setAttribute("aria-describedby", dragDisabled? INSTRUCTION_IDs.DND_ZONE_DRAG_DISABLED : INSTRUCTION_IDs.DND_ZONE_ACTIVE); | ||
node.setAttribute("aria-describedby", dragDisabled ? INSTRUCTION_IDs.DND_ZONE_DRAG_DISABLED : INSTRUCTION_IDs.DND_ZONE_ACTIVE); | ||
} | ||
@@ -295,9 +297,16 @@ if (config.type && newType !== config.type) { | ||
node.tabIndex = isDragging && (node === focusedDz || focusedItem.contains(node) || config.dropFromOthersDisabled || (focusedDz && config.type !== dzToConfig.get(focusedDz).type)) ? -1 : 0; | ||
node.addEventListener('focus', handleZoneFocus); | ||
node.tabIndex = | ||
isDragging && | ||
(node === focusedDz || | ||
focusedItem.contains(node) || | ||
config.dropFromOthersDisabled || | ||
(focusedDz && config.type !== dzToConfig.get(focusedDz).type)) | ||
? -1 | ||
: 0; | ||
node.addEventListener("focus", handleZoneFocus); | ||
for (let i = 0; i < node.children.length ; i++) { | ||
for (let i = 0; i < node.children.length; i++) { | ||
const draggableEl = node.children[i]; | ||
allDragTargets.add(draggableEl); | ||
draggableEl.tabIndex = (isDragging) ? -1 : 0; | ||
draggableEl.tabIndex = isDragging ? -1 : 0; | ||
if (!autoAriaDisabled) { | ||
@@ -326,3 +335,3 @@ draggableEl.setAttribute("role", "listitem"); | ||
const handles = { | ||
update: (newOptions) => { | ||
update: newOptions => { | ||
printDebug(() => `keyboard dndzone will update newOptions: ${toString(newOptions)}`); | ||
@@ -340,2 +349,2 @@ configure(newOptions); | ||
return handles; | ||
} | ||
} |
@@ -6,6 +6,7 @@ import { | ||
incrementActiveDropZoneCount, | ||
decrementActiveDropZoneCount, SOURCES | ||
} from './constants' | ||
import { observe, unobserve } from './helpers/observer'; | ||
import { armWindowScroller, disarmWindowScroller} from "./helpers/windowScroller"; | ||
decrementActiveDropZoneCount, | ||
SOURCES | ||
} from "./constants"; | ||
import {observe, unobserve} from "./helpers/observer"; | ||
import {armWindowScroller, disarmWindowScroller} from "./helpers/windowScroller"; | ||
import { | ||
@@ -21,11 +22,18 @@ createDraggedElementFrom, | ||
} from "./helpers/styler"; | ||
import { DRAGGED_ENTERED_EVENT_NAME, DRAGGED_LEFT_EVENT_NAME, DRAGGED_LEFT_DOCUMENT_EVENT_NAME, DRAGGED_OVER_INDEX_EVENT_NAME, dispatchConsiderEvent, dispatchFinalizeEvent } from './helpers/dispatcher'; | ||
import { | ||
DRAGGED_ENTERED_EVENT_NAME, | ||
DRAGGED_LEFT_EVENT_NAME, | ||
DRAGGED_LEFT_DOCUMENT_EVENT_NAME, | ||
DRAGGED_OVER_INDEX_EVENT_NAME, | ||
dispatchConsiderEvent, | ||
dispatchFinalizeEvent | ||
} from "./helpers/dispatcher"; | ||
import {areObjectsShallowEqual, toString} from "./helpers/util"; | ||
import {printDebug} from "./constants"; | ||
const DEFAULT_DROP_ZONE_TYPE = '--any--'; | ||
const DEFAULT_DROP_ZONE_TYPE = "--any--"; | ||
const MIN_OBSERVATION_INTERVAL_MS = 100; | ||
const MIN_MOVEMENT_BEFORE_DRAG_START_PX = 3; | ||
const DEFAULT_DROP_TARGET_STYLE = { | ||
outline: 'rgba(255, 255, 102, 0.7) solid 2px', | ||
outline: "rgba(255, 255, 102, 0.7) solid 2px" | ||
}; | ||
@@ -55,3 +63,3 @@ | ||
function registerDropZone(dropZoneEl, type) { | ||
printDebug(() => 'registering drop-zone if absent') | ||
printDebug(() => "registering drop-zone if absent"); | ||
if (!typeToDropZones.has(type)) { | ||
@@ -75,3 +83,3 @@ typeToDropZones.set(type, new Set()); | ||
function watchDraggedElement() { | ||
printDebug(() => 'watching dragged element'); | ||
printDebug(() => "watching dragged element"); | ||
armWindowScroller(); | ||
@@ -86,7 +94,10 @@ const dropZones = typeToDropZones.get(draggedElType); | ||
// it is important that we don't have an interval that is faster than the flip duration because it can cause elements to jump bach and forth | ||
const observationIntervalMs = Math.max(MIN_OBSERVATION_INTERVAL_MS, ...Array.from(dropZones.keys()).map(dz => dzToConfig.get(dz).dropAnimationDurationMs)); | ||
const observationIntervalMs = Math.max( | ||
MIN_OBSERVATION_INTERVAL_MS, | ||
...Array.from(dropZones.keys()).map(dz => dzToConfig.get(dz).dropAnimationDurationMs) | ||
); | ||
observe(draggedEl, dropZones, observationIntervalMs * 1.07); | ||
} | ||
function unWatchDraggedElement() { | ||
printDebug(() => 'unwatching dragged element'); | ||
printDebug(() => "unwatching dragged element"); | ||
disarmWindowScroller(); | ||
@@ -105,22 +116,22 @@ const dropZones = typeToDropZones.get(draggedElType); | ||
function handleDraggedEntered(e) { | ||
printDebug(() => ['dragged entered', e.currentTarget, e.detail]); | ||
printDebug(() => ["dragged entered", e.currentTarget, e.detail]); | ||
let {items, dropFromOthersDisabled} = dzToConfig.get(e.currentTarget); | ||
if (dropFromOthersDisabled && e.currentTarget !== originDropZone) { | ||
printDebug(() => 'drop is currently disabled'); | ||
printDebug(() => "drop is currently disabled"); | ||
return; | ||
} | ||
// this deals with another race condition. in rare occasions (super rapid operations) the list hasn't updated yet | ||
items = items.filter(i => i[ITEM_ID_KEY] !== shadowElData[ITEM_ID_KEY]) | ||
items = items.filter(i => i[ITEM_ID_KEY] !== shadowElData[ITEM_ID_KEY]); | ||
printDebug(() => `dragged entered items ${toString(items)}`); | ||
const {index, isProximityBased} = e.detail.indexObj; | ||
const shadowElIdx = (isProximityBased && index === e.currentTarget.children.length - 1)? index + 1 : index; | ||
const shadowElIdx = isProximityBased && index === e.currentTarget.children.length - 1 ? index + 1 : index; | ||
shadowElDropZone = e.currentTarget; | ||
items.splice( shadowElIdx, 0, shadowElData); | ||
items.splice(shadowElIdx, 0, shadowElData); | ||
dispatchConsiderEvent(e.currentTarget, items, {trigger: TRIGGERS.DRAGGED_ENTERED, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER}); | ||
} | ||
function handleDraggedLeft(e) { | ||
printDebug(() => ['dragged left', e.currentTarget, e.detail]); | ||
printDebug(() => ["dragged left", e.currentTarget, e.detail]); | ||
const {items, dropFromOthersDisabled} = dzToConfig.get(e.currentTarget); | ||
if (dropFromOthersDisabled && e.currentTarget !== originDropZone) { | ||
printDebug(() => 'drop is currently disabled'); | ||
printDebug(() => "drop is currently disabled"); | ||
return; | ||
@@ -134,6 +145,6 @@ } | ||
function handleDraggedIsOverIndex(e) { | ||
printDebug(() => ['dragged is over index', e.currentTarget, e.detail]); | ||
printDebug(() => ["dragged is over index", e.currentTarget, e.detail]); | ||
const {items, dropFromOthersDisabled} = dzToConfig.get(e.currentTarget); | ||
if (dropFromOthersDisabled && e.currentTarget !== originDropZone) { | ||
printDebug(() => 'drop is currently disabled'); | ||
printDebug(() => "drop is currently disabled"); | ||
return; | ||
@@ -144,3 +155,3 @@ } | ||
items.splice(shadowElIdx, 1); | ||
items.splice( index, 0, shadowElData); | ||
items.splice(index, 0, shadowElData); | ||
dispatchConsiderEvent(e.currentTarget, items, {trigger: TRIGGERS.DRAGGED_OVER_INDEX, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER}); | ||
@@ -152,20 +163,23 @@ } | ||
e.preventDefault(); | ||
const c = e.touches? e.touches[0] : e; | ||
const c = e.touches ? e.touches[0] : e; | ||
currentMousePosition = {x: c.clientX, y: c.clientY}; | ||
draggedEl.style.transform = `translate3d(${currentMousePosition.x - dragStartMousePosition.x}px, ${currentMousePosition.y - dragStartMousePosition.y}px, 0)`; | ||
draggedEl.style.transform = `translate3d(${currentMousePosition.x - dragStartMousePosition.x}px, ${ | ||
currentMousePosition.y - dragStartMousePosition.y | ||
}px, 0)`; | ||
} | ||
function handleDrop() { | ||
printDebug(() => 'dropped'); | ||
printDebug(() => "dropped"); | ||
finalizingPreviousDrag = true; | ||
// cleanup | ||
window.removeEventListener('mousemove', handleMouseMove); | ||
window.removeEventListener('touchmove', handleMouseMove); | ||
window.removeEventListener('mouseup', handleDrop); | ||
window.removeEventListener('touchend', handleDrop); | ||
window.removeEventListener("mousemove", handleMouseMove); | ||
window.removeEventListener("touchmove", handleMouseMove); | ||
window.removeEventListener("mouseup", handleDrop); | ||
window.removeEventListener("touchend", handleDrop); | ||
unWatchDraggedElement(); | ||
moveDraggedElementToWasDroppedState(draggedEl); | ||
let finalizeFunction; | ||
if (shadowElDropZone) { // it was dropped in a drop-zone | ||
printDebug(() => ['dropped in dz', shadowElDropZone]); | ||
if (shadowElDropZone) { | ||
// it was dropped in a drop-zone | ||
printDebug(() => ["dropped in dz", shadowElDropZone]); | ||
let {items, type} = dzToConfig.get(shadowElDropZone); | ||
@@ -176,16 +190,24 @@ styleInactiveDropZones(typeToDropZones.get(type), dz => dzToConfig.get(dz).dropTargetStyle); | ||
if (shadowElIdx === -1) shadowElIdx = originIndex; | ||
items = items.map(item => item[SHADOW_ITEM_MARKER_PROPERTY_NAME]? draggedElData : item); | ||
items = items.map(item => (item[SHADOW_ITEM_MARKER_PROPERTY_NAME] ? draggedElData : item)); | ||
finalizeFunction = function finalizeWithinZone() { | ||
dispatchFinalizeEvent(shadowElDropZone, items, {trigger: TRIGGERS.DROPPED_INTO_ZONE, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER}); | ||
dispatchFinalizeEvent(shadowElDropZone, items, { | ||
trigger: TRIGGERS.DROPPED_INTO_ZONE, | ||
id: draggedElData[ITEM_ID_KEY], | ||
source: SOURCES.POINTER | ||
}); | ||
if (shadowElDropZone !== originDropZone) { | ||
// letting the origin drop zone know the element was permanently taken away | ||
dispatchFinalizeEvent(originDropZone, dzToConfig.get(originDropZone).items, {trigger: TRIGGERS.DROPPED_INTO_ANOTHER, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER}); | ||
dispatchFinalizeEvent(originDropZone, dzToConfig.get(originDropZone).items, { | ||
trigger: TRIGGERS.DROPPED_INTO_ANOTHER, | ||
id: draggedElData[ITEM_ID_KEY], | ||
source: SOURCES.POINTER | ||
}); | ||
} | ||
shadowElDropZone.children[shadowElIdx].style.visibility = ''; | ||
shadowElDropZone.children[shadowElIdx].style.visibility = ""; | ||
cleanupPostDrop(); | ||
} | ||
}; | ||
animateDraggedToFinalPosition(shadowElIdx, finalizeFunction); | ||
} | ||
else { // it needs to return to its place | ||
printDebug(() => 'no dz available'); | ||
} else { | ||
// it needs to return to its place | ||
printDebug(() => "no dz available"); | ||
let {items, type} = dzToConfig.get(originDropZone); | ||
@@ -195,10 +217,18 @@ styleInactiveDropZones(typeToDropZones.get(type), dz => dzToConfig.get(dz).dropTargetStyle); | ||
shadowElDropZone = originDropZone; | ||
dispatchConsiderEvent(originDropZone, items, {trigger: TRIGGERS.DROPPED_OUTSIDE_OF_ANY, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER}); | ||
dispatchConsiderEvent(originDropZone, items, { | ||
trigger: TRIGGERS.DROPPED_OUTSIDE_OF_ANY, | ||
id: draggedElData[ITEM_ID_KEY], | ||
source: SOURCES.POINTER | ||
}); | ||
finalizeFunction = function finalizeBackToOrigin() { | ||
const finalItems = [...items]; | ||
finalItems.splice(originIndex, 1, draggedElData); | ||
dispatchFinalizeEvent(originDropZone, finalItems, {trigger: TRIGGERS.DROPPED_OUTSIDE_OF_ANY, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER}); | ||
shadowElDropZone.children[originIndex].style.visibility = ''; | ||
dispatchFinalizeEvent(originDropZone, finalItems, { | ||
trigger: TRIGGERS.DROPPED_OUTSIDE_OF_ANY, | ||
id: draggedElData[ITEM_ID_KEY], | ||
source: SOURCES.POINTER | ||
}); | ||
shadowElDropZone.children[originIndex].style.visibility = ""; | ||
cleanupPostDrop(); | ||
} | ||
}; | ||
window.setTimeout(() => animateDraggedToFinalPosition(originIndex, finalizeFunction), 0); | ||
@@ -216,4 +246,4 @@ } | ||
const {dropAnimationDurationMs} = dzToConfig.get(shadowElDropZone); | ||
const transition = `transform ${dropAnimationDurationMs}ms ease` | ||
draggedEl.style.transition = draggedEl.style.transition? draggedEl.style.transition + "," + transition : transition; | ||
const transition = `transform ${dropAnimationDurationMs}ms ease`; | ||
draggedEl.style.transition = draggedEl.style.transition ? draggedEl.style.transition + "," + transition : transition; | ||
draggedEl.style.transform = `translate3d(${newTransform.x}px, ${newTransform.y}px, 0)`; | ||
@@ -242,3 +272,3 @@ window.setTimeout(callback, dropAnimationDurationMs); | ||
export function dndzone(node, options) { | ||
const config = { | ||
const config = { | ||
items: undefined, | ||
@@ -250,3 +280,3 @@ type: undefined, | ||
dropTargetStyle: DEFAULT_DROP_TARGET_STYLE, | ||
transformDraggedElement : () => {} | ||
transformDraggedElement: () => {} | ||
}; | ||
@@ -257,12 +287,12 @@ printDebug(() => [`dndzone good to go options: ${toString(options)}, config: ${toString(config)}`, {node}]); | ||
function addMaybeListeners() { | ||
window.addEventListener('mousemove', handleMouseMoveMaybeDragStart, {passive: false}); | ||
window.addEventListener('touchmove', handleMouseMoveMaybeDragStart, {passive: false, capture: false}); | ||
window.addEventListener('mouseup', handleFalseAlarm, {passive: false}); | ||
window.addEventListener('touchend', handleFalseAlarm, {passive: false}); | ||
window.addEventListener("mousemove", handleMouseMoveMaybeDragStart, {passive: false}); | ||
window.addEventListener("touchmove", handleMouseMoveMaybeDragStart, {passive: false, capture: false}); | ||
window.addEventListener("mouseup", handleFalseAlarm, {passive: false}); | ||
window.addEventListener("touchend", handleFalseAlarm, {passive: false}); | ||
} | ||
function removeMaybeListeners() { | ||
window.removeEventListener('mousemove', handleMouseMoveMaybeDragStart); | ||
window.removeEventListener('touchmove', handleMouseMoveMaybeDragStart); | ||
window.removeEventListener('mouseup', handleFalseAlarm); | ||
window.removeEventListener('touchend', handleFalseAlarm); | ||
window.removeEventListener("mousemove", handleMouseMoveMaybeDragStart); | ||
window.removeEventListener("touchmove", handleMouseMoveMaybeDragStart); | ||
window.removeEventListener("mouseup", handleFalseAlarm); | ||
window.removeEventListener("touchend", handleFalseAlarm); | ||
} | ||
@@ -278,5 +308,8 @@ function handleFalseAlarm() { | ||
e.preventDefault(); | ||
const c = e.touches? e.touches[0] : e; | ||
const c = e.touches ? e.touches[0] : e; | ||
currentMousePosition = {x: c.clientX, y: c.clientY}; | ||
if (Math.abs(currentMousePosition.x - dragStartMousePosition.x) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX || Math.abs(currentMousePosition.y - dragStartMousePosition.y) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX) { | ||
if ( | ||
Math.abs(currentMousePosition.x - dragStartMousePosition.x) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX || | ||
Math.abs(currentMousePosition.y - dragStartMousePosition.y) >= MIN_MOVEMENT_BEFORE_DRAG_START_PX | ||
) { | ||
removeMaybeListeners(); | ||
@@ -293,8 +326,8 @@ handleDragStart(); | ||
if (isWorkingOnPreviousDrag) { | ||
printDebug(() => 'cannot start a new drag before finalizing previous one'); | ||
printDebug(() => "cannot start a new drag before finalizing previous one"); | ||
return; | ||
} | ||
e.stopPropagation(); | ||
const c = e.touches? e.touches[0] : e; | ||
dragStartMousePosition = {x: c.clientX, y:c.clientY}; | ||
const c = e.touches ? e.touches[0] : e; | ||
dragStartMousePosition = {x: c.clientX, y: c.clientY}; | ||
currentMousePosition = {...dragStartMousePosition}; | ||
@@ -337,5 +370,4 @@ originalDragTarget = e.currentTarget; | ||
styleActiveDropZones( | ||
Array.from(typeToDropZones.get(config.type)) | ||
.filter(dz => dz === originDropZone || !dzToConfig.get(dz).dropFromOthersDisabled), | ||
dz => dzToConfig.get(dz).dropTargetStyle, | ||
Array.from(typeToDropZones.get(config.type)).filter(dz => dz === originDropZone || !dzToConfig.get(dz).dropFromOthersDisabled), | ||
dz => dzToConfig.get(dz).dropTargetStyle | ||
); | ||
@@ -348,17 +380,17 @@ | ||
// handing over to global handlers - starting to watch the element | ||
window.addEventListener('mousemove', handleMouseMove, {passive: false}); | ||
window.addEventListener('touchmove', handleMouseMove, {passive: false, capture: false}); | ||
window.addEventListener('mouseup', handleDrop, {passive: false}); | ||
window.addEventListener('touchend', handleDrop, {passive: false}); | ||
window.addEventListener("mousemove", handleMouseMove, {passive: false}); | ||
window.addEventListener("touchmove", handleMouseMove, {passive: false, capture: false}); | ||
window.addEventListener("mouseup", handleDrop, {passive: false}); | ||
window.addEventListener("touchend", handleDrop, {passive: false}); | ||
} | ||
function configure({ | ||
items = undefined, | ||
flipDurationMs:dropAnimationDurationMs = 0, | ||
type: newType = DEFAULT_DROP_ZONE_TYPE, | ||
dragDisabled = false, | ||
dropFromOthersDisabled = false, | ||
dropTargetStyle = DEFAULT_DROP_TARGET_STYLE, | ||
transformDraggedElement = () => {}, | ||
}) { | ||
items = undefined, | ||
flipDurationMs: dropAnimationDurationMs = 0, | ||
type: newType = DEFAULT_DROP_ZONE_TYPE, | ||
dragDisabled = false, | ||
dropFromOthersDisabled = false, | ||
dropTargetStyle = DEFAULT_DROP_TARGET_STYLE, | ||
transformDraggedElement = () => {} | ||
}) { | ||
config.dropAnimationDurationMs = dropAnimationDurationMs; | ||
@@ -397,11 +429,13 @@ if (config.type && newType !== config.type) { | ||
if (config.items[idx][SHADOW_ITEM_MARKER_PROPERTY_NAME]) { | ||
morphDraggedElementToBeLike(draggedEl, draggableEl, currentMousePosition.x, currentMousePosition.y, () => config.transformDraggedElement(draggedEl, draggedElData, idx)); | ||
morphDraggedElementToBeLike(draggedEl, draggableEl, currentMousePosition.x, currentMousePosition.y, () => | ||
config.transformDraggedElement(draggedEl, draggedElData, idx) | ||
); | ||
styleShadowEl(draggableEl); | ||
continue; | ||
} | ||
draggableEl.removeEventListener('mousedown', elToMouseDownListener.get(draggableEl)); | ||
draggableEl.removeEventListener('touchstart', elToMouseDownListener.get(draggableEl)); | ||
draggableEl.removeEventListener("mousedown", elToMouseDownListener.get(draggableEl)); | ||
draggableEl.removeEventListener("touchstart", elToMouseDownListener.get(draggableEl)); | ||
if (!dragDisabled) { | ||
draggableEl.addEventListener('mousedown', handleMouseDown); | ||
draggableEl.addEventListener('touchstart', handleMouseDown); | ||
draggableEl.addEventListener("mousedown", handleMouseDown); | ||
draggableEl.addEventListener("touchstart", handleMouseDown); | ||
elToMouseDownListener.set(draggableEl, handleMouseDown); | ||
@@ -415,4 +449,4 @@ } | ||
return ({ | ||
update: (newOptions) => { | ||
return { | ||
update: newOptions => { | ||
printDebug(() => `pointer dndzone will update newOptions: ${toString(newOptions)}`); | ||
@@ -426,3 +460,3 @@ configure(newOptions); | ||
} | ||
}); | ||
}; | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
233714
5506
271
12