├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── Easy-DND.iml ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── .vitepress │ └── config.js ├── advanced-demos.md ├── components │ ├── drag.md │ ├── drop.md │ ├── droplist.md │ └── dropmask.md ├── events.md ├── faq.md ├── img │ ├── vid1.gif │ ├── vid10.gif │ ├── vid11.gif │ ├── vid12.gif │ ├── vid2.gif │ ├── vid3.gif │ ├── vid4.gif │ ├── vid5.gif │ ├── vid6.gif │ ├── vid7.gif │ ├── vid8.gif │ └── vid9.gif ├── index.md └── installation.md ├── lib ├── README.md ├── package.json ├── rollup.config.mjs └── src │ ├── components │ ├── Drag.vue │ ├── DragFeedback.vue │ ├── Drop.vue │ ├── DropList.vue │ └── DropMask.vue │ ├── helpers │ ├── edgescroller.js │ └── scrollparent.js │ ├── index.js │ ├── js │ ├── DnD.js │ ├── DragImagesManager.js │ ├── Grid.js │ ├── createDragImage.js │ └── events.js │ └── mixins │ ├── DragAwareMixin.js │ ├── DragMixin.js │ └── DropMixin.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── App10.vue ├── App11.vue ├── App12.vue ├── App13.vue ├── App14.vue ├── App15.vue ├── App16.vue ├── App17.vue ├── App2.vue ├── App3.vue ├── App4.vue ├── App5.vue ├── App6.vue ├── App7.vue ├── App8.vue ├── App9.vue ├── assets │ └── logo.svg ├── components │ ├── App12Item.vue │ ├── Atomic.vue │ ├── Column.vue │ ├── DropZone.vue │ ├── Flex.vue │ ├── Generic.vue │ ├── MyDiv.vue │ ├── Row.vue │ └── scaffold │ │ ├── Avatar.vue │ │ ├── Chip.vue │ │ ├── List.vue │ │ ├── ListItem.vue │ │ ├── Page.vue │ │ ├── Separator.vue │ │ └── Skeleton.vue └── main.js └── vue.config.js /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a VitePress site to GitHub Pages 2 | # 3 | name: Deploy VitePress site to Pages 4 | 5 | on: 6 | # Runs on pushes targeting the `master` branch. Change this to `master` if you're 7 | # using the `master` branch as the default branch. 8 | push: 9 | branches: [master] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 15 | permissions: 16 | contents: read 17 | pages: write 18 | id-token: write 19 | 20 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 21 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 22 | concurrency: 23 | group: pages 24 | cancel-in-progress: false 25 | 26 | jobs: 27 | # Build job 28 | build: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | with: 34 | fetch-depth: 0 # Not needed if lastUpdated is not enabled 35 | # - uses: pnpm/action-setup@v3 # Uncomment this block if you're using pnpm 36 | # with: 37 | # version: 9 # Not needed if you've set "packageManager" in package.json 38 | # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun 39 | - name: Setup Node 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: 22 43 | cache: npm # or pnpm / yarn 44 | - name: Setup Pages 45 | uses: actions/configure-pages@v4 46 | - name: Install dependencies 47 | run: npm ci # or pnpm install / yarn install / bun install 48 | - name: Build with VitePress 49 | run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build 50 | - name: Upload artifact 51 | uses: actions/upload-pages-artifact@v3 52 | with: 53 | path: docs/.vitepress/dist 54 | 55 | # Deployment job 56 | deploy: 57 | environment: 58 | name: github-pages 59 | url: ${{ steps.deployment.outputs.page_url }} 60 | needs: build 61 | runs-on: ubuntu-latest 62 | name: Deploy 63 | steps: 64 | - name: Deploy to GitHub Pages 65 | id: deployment 66 | uses: actions/deploy-pages@v4 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | yarn.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | 65 | .idea/ 66 | *.iml 67 | lib/dist/ 68 | 69 | docs/.vitepress/cache/ 70 | docs/.vitepress/dist/ 71 | -------------------------------------------------------------------------------- /Easy-DND.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Régis Lemaigre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue-Easy-DnD 2 | ### A HTML5 drag-and-drop replacement 3 | Easy-DnD is a drag and drop implementation for Vue that uses only standard mouse events instead of the HTML5 drag and drop API, which is [impossible to work with](https://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html). 4 | 5 | Think of it as a way to transfer data from some components to others using the mouse or support for a mouse assisted copy/cut - paste. It also allows for lists to be reordered by drag and drop. 6 | 7 | ## Documentation 8 | Our documentation has moved to Github Pages! 9 | 10 | ### [View Documentation Here](https://rlemaigre.github.io/Easy-DnD) 11 | 12 | ## Examples 13 | View more examples within our documentation 14 | 15 | ![demo](docs/img/vid4.gif) 16 | ![demo](docs/img/vid7.gif) 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | // https://vitepress.dev/reference/site-config 2 | export default { 3 | title: "Vue-Easy-DnD", 4 | description: "A HTML5 drag-and-drop replacement", 5 | base: "/Easy-DnD/", 6 | themeConfig: { 7 | // https://vitepress.dev/reference/default-theme-config 8 | nav: [ 9 | { text: 'Home', link: '/' }, 10 | { text: 'API', link: '/installation' }, 11 | { text: 'FAQ', link: '/faq' } 12 | ], 13 | 14 | sidebar: [ 15 | { 16 | text: 'Getting Started', 17 | items: [ 18 | { text: 'Installation', link: '/installation' }, 19 | { text: 'FAQ', link: '/faq' }, 20 | { text: 'Events / Mixins', link: '/events' }, 21 | { text: 'Advanced Demos', link: '/advanced-demos' } 22 | ] 23 | }, 24 | { 25 | text: 'Components', 26 | items: [ 27 | { text: 'Drag', link: '/components/drag' }, 28 | { text: 'Drop', link: '/components/drop' }, 29 | { text: 'DropList', link: '/components/droplist' }, 30 | { text: 'DropMask', link: '/components/dropmask' } 31 | ] 32 | } 33 | ], 34 | 35 | socialLinks: [ 36 | { icon: 'github', link: 'https://github.com/rlemaigre/Easy-DnD' } 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/advanced-demos.md: -------------------------------------------------------------------------------- 1 | # Advanced Demos 2 | 3 | These demos are meant to be pretty and reflect real life examples. 4 | 5 | The following demo features list reordering, drag and drop between two lists, custom drag images and custom feedback when inserting new items. 6 | 7 | https://codesandbox.io/s/easy-dnd-demo-9mbij 8 | 9 | ![demo](img/vid7.gif) 10 | 11 | The following demo features drag and drop between a table and a list, custom drag images and a custom style for drop areas when the drop is allowed. 12 | 13 | https://codesandbox.io/s/easy-dnd-demo-2-xnqbz 14 | 15 | ![demo](img/vid8.gif) 16 | 17 | The following demo features nested drop lists and a custom drag image. It is a tool to design dashboards by WYSIWYG. New widgets can be dragged into the dashboard from the palette on the left and widgets can be moved around by drag and drop. 18 | 19 | ![demo](img/vid10.gif) 20 | -------------------------------------------------------------------------------- /docs/components/drag.md: -------------------------------------------------------------------------------- 1 | # Drag 2 | The `drag` component is meant to define an area from which data can be exported. 3 | 4 | ## Events 5 | Event Name | Description 6 | ---------- | ----------- 7 | `@dragstart` | Triggered when a drag operation starts 8 | `@dragend` | Triggered when a drag operation terminates (whether successfully or not) 9 | `@cut` / `@copy` | Triggered when a drag operation completes successfully on a Drop component that requires the data to be removed / copied (event name is dependant on the selected drop `mode` prop) 10 | 11 | ## Props 12 | Prop Name | Type / Default | Description 13 | --------- | -------------- | ----------- 14 | `tag` | Any | This prop can be used to customize the root of the template, Can refer to a custom Vue component, including its props, slots and listeners. 15 | `type` | String (`null`) | Refer to **Types** section below 16 | `data` | Any | Any data associated with this drag which will be sent with the emit event 17 | `drag-image-opacity` | Number (`0.7`) | 0-1 defining the opacity of the drag image 18 | `disabled` | Boolean (`false`) | Whether to temporarily disable dragging this component 19 | `go-back` | Boolean (`false`) | If a drag is not successful, the drag image will animate back to where the drag originated (demo below) 20 | `handle` | String (`undefined`) | A handle / grabber for this Drag component (eg: `.drag-handle`) 21 | `delta` | Number (`3`px) | A pixel-distance which defines whether a drag has begun 22 | `delay` | Number (`0`ms) | The number of milliseconds of which the user must hold down the Drag element until it is recognised as a drag (useful for allowing scrolling on Touch devices without it automatically trying to drag the element) (`0` = no delay) 23 | `drag-class` | String (`null`) | A class to bind to the image / ghost being dragged around 24 | `vibration` | Number (`0`ms) | Vibration feedback on supported mobile devices when a Drag event has started (`0` = no feedback) 25 | `scrolling-edge-size` | Number (`100`px) | When dragging this element to the edge of its bounding container/list, the pixel amount defines how close to the edge of the container it will automatically scroll up/down/left/right (`0` = no scrolling on its bounding container) 26 | 27 | ## Slots 28 | Slot Name | Description 29 | ---------- | ----------- 30 | `default` | Default content to add at the end of the DropList. Make sure to define a `key` prop for each element directly inside this slot. 31 | `drag-image` | Refer to **Drag Image** section below 32 | 33 | ## Demo 34 | An example of `go-back` prop 35 | 36 | https://codesandbox.io/s/example-1-l6p54 37 | 38 | ![demo](../img/vid12.gif) 39 | 40 | ## CSS classes 41 | 42 | Depending on the mode of the Drop component currently under the mouse cursor, the source Drag component is assigned the classes `drag-mode-copy`, `drag-mode-cut` and `drag-mode-reordering`. 43 | 44 | If a drag is in progress, `dnd-ghost` will be bound to the ghost. The Drag component can also optionally accept an additional class (`drag-class` prop) to bind to the ghost. When using a DropList, the `dnd-ghost` class and `drag-class` prop will both **NOT** be bound if the `drag-image` slot is defined. 45 | 46 | To prevent dragging your Draggable component from a child element, you may attach the class `dnd-no-drag` to the child. That way the entire element remains draggable, whereas specific sub-children can have dragging from themselves disabled. 47 | 48 | ## Types 49 | 50 | A drag operation **may** have a type. The type is a data structure (can be a simple string) that defines the kind of data being transfered. The type of a drag operation is defined by the Drag component that initiates it using the `type` prop. 51 | 52 | A Drop component is said to participate in a drag operation if it accepts its type (the default is to accept any type). The type(s) a Drop component accepts can be defined by mean of the `accepts-type` prop (can be a string, an array of strings or a function that takes the type as parameter and returns a boolean). 53 | 54 | The following demo illustrates the use of types. There are two types in use : 'string' and 'number'. The Drag components that contain '1' and '2' are of type 'number', the ones containing 'a' and 'b' are of type 'string'. The two Drop components on the left accept numbers, the ones on the right accept strings. When you drag a number/string (respectively), only Drop components that accept numbers/strings (respectively) react (i.e. drag images, CSS classes, cursors are applied). The other ones are left alone. 55 | 56 | https://codesandbox.io/s/example-3-g7io8 57 | 58 | ![demo](../img/vid4.gif) 59 | 60 | ## Drag image 61 | 62 | During the drag, an image may move along the mouse cursor. Easy-DnD makes it so that this image is always on top of everything else. 63 | 64 | Drag components provide the `drag-image` slot that can be used to set the default image displayed during the drag operation : 65 | 66 | * if the slot isn't defined, the image is a clone of the Drag component. 67 | * if the slot is defined and empty, there is no image. 68 | * if the slot is not empty, a clone of its content is used. 69 | 70 | Drop and DropList components provide the `drag-image` slot (props : `data` and `type`) that can be used to set the image to be displayed when the mouse is over them, if they participates in the current drag operation (i.e. accept its type) : 71 | 72 | * if the slot isn't defined, the default image set by the Drag component is used. 73 | * if the slot is defined and empty, there is no image. 74 | * if the slot is not empty, a clone of its content is used. 75 | 76 | DropList components additionaly provide the `reordering-drag-image` slot (prop : `item` subject to reordering) that behaves the same way as `drag-image` but controls the drag image to be displayed during list reordering. 77 | 78 | The position of the drag image relative to the mouse cursor can be controlled by CSS using the transform property. 79 | 80 | The following demo illustrate the use of custom drag images, nested Drop components and a mask : 81 | 82 | https://codesandbox.io/s/example-4-6h8zy 83 | 84 | ![demo](../img/vid5.gif) 85 | -------------------------------------------------------------------------------- /docs/components/drop.md: -------------------------------------------------------------------------------- 1 | # Drop 2 | The `drop` component is meant to define an area into which data can be imported. Drop components can be nested forming hierarchies of arbitrary depth. 3 | 4 | ## Events 5 | Event Name | Description 6 | ---------- | ----------- 7 | `@dragenter` | Triggered when the mouse enters a Drop component 8 | `@dragleave` | Triggered when the mouse leaves a Drop component 9 | `@dragover` | Triggered when the mouse moves over a Drop component 10 | `@dragend` | Triggered when a drag event is finished while a Drag element is hovered over this Drop component. (Including if ESC is pressed while a Drag element was over the top of this Drop component) 11 | `@drop` | Triggered when a drop operation completes on a Drop component 12 | 13 | ## Props 14 | Prop Name | Type / Default | Description 15 | --------- | -------------- | ----------- 16 | `tag` | Any | This prop can be used to customize the root of the template, Can refer to a custom Vue component, including its props, slots and listeners. 17 | `accepts-type` | String / Array | Refer to **Types** section above 18 | `accepts-data` | Any | Refer to **Restricting droppable data** section below 19 | `drag-image-opacity` | Number (`0.7`) | 0-1 defining the opacity of the drag image when dragging over this drop component 20 | `mode` | String (`copy`) | Refer to **Modes** section below 21 | 22 | ## CSS classes 23 | During a drag operation, the Drop components on the page are assigned several CSS classes : 24 | 25 | * for all Drop components : `type-allowed` if the Drop component accepts the type of the drag operation, `type-forbidden` otherwise 26 | * for the Drop components that participate in the drag operation (i.e. accepts its type) : 27 | * `drop-in` when the mouse is over one that is foremost at the current mouse position (remember Drop components can be nested), `drop-out` otherwise 28 | * `drop-allowed` when the Drop component accepts the data and the source of the drag accepts its mode, `drop-forbidden` otherwise 29 | 30 | 31 | 32 | ## Modes 33 | 34 | A drag and drop can occur in several possible modes, depending on its effect on the origin of the drag : 35 | 36 | * `copy` (the default) : if the source of the drag is unaffected by the drag operation, 37 | * `cut` : if the source of the drag is to be removed when the drag operation completes. 38 | 39 | Drop components must declare what mode must be triggered when data is dropped into them using the `mode` property. 40 | 41 | When a drag operation completes on a Drop component that declares the `cut` (respectively `copy`) mode, a `cut` (respectively `copy`) event is emitted on the Drag component from which the drag operation originated. This gives the opportunity to the surroundings of the Drag component to react to the drop that just happened, for example by removing the data that has been dropped in case of the `cut` event. 42 | 43 | If a drag operation originates from a Drag components that doesn't declare a listener for the `cut` event, then dropping is forbidden on a Drop component that declares the `cut` mode. 44 | 45 | The following demo illustrates modes in action : 46 | 47 | https://codesandbox.io/s/example-2-r8n1k 48 | 49 | ![demo](../img/vid3.gif) 50 | 51 | ## Restricting droppable data 52 | 53 | Drop components can restrict the data they accept by mean of the `accepts-data` prop (a function that takes the data and type as parameter and returns a boolean). 54 | 55 | The following demo defines five Drag components that can be dragged into three Drop components, one that accepts even numbers, one that accepts odd numbers and one that accepts any number but removes them once the drag is complete. 56 | 57 | https://codesandbox.io/s/easy-dnd-demo-fo078 58 | 59 | ![demo](../img/vid1.gif) 60 | -------------------------------------------------------------------------------- /docs/components/droplist.md: -------------------------------------------------------------------------------- 1 | # DropList 2 | The `drop-list` component is a special kind of drop component that displays a list of items that support dragging into and reordering. 3 | 4 | ## Events 5 | Event Name | Description 6 | ---------- | ----------- 7 | (Refer to `Drop` component) | DropList components also emit all events by Drop components 8 | `@insert` | Triggered when data is to be inserted into the list (properties : `type`, `data` and `index`). If no listener is provided for this event, the list cannot be inserted into. 9 | `@reorder` | Triggers when data needs to be reordered (properties : `from`, `to` and `apply` - apply is a function that applies the required reordering to the given array). If no listener is provided for this event, the list cannot be reordered. 10 | 11 | ## Props 12 | Prop Name | Type / Default | Description 13 | --------- | -------------- | ----------- 14 | (Refer to `Drop` component) | |DropList components also inherit all props from Drop components 15 | `tag` | Any | This prop can be used to customize the root of the template, just like it can be with drop components, but it can only refer to an HTML element, not a Vue component (this is a restriction of Vue transition-groups - there is nothing I can do about it). However, when the no-animations prop is set to true, this restriction is lifted, and you can use any Vue component. 16 | `items` | Any (Array) | Array of data to use on this DropList 17 | `row` | Boolean (`null`) | Defining the direction of the DropList as horizontally-flowing. (Necessary for Nested DropLists) (Refer to **Nested Droplists** section below) 18 | `column` | Boolean (`null`) | Defining the direction of the DropList as vertically-flowing. (Necessary for Nested DropLists) (Refer to **Nested Droplists** section below) 19 | `no-animations` | Boolean (`false`) | Disable animations on the DropList (necessary if the tag is a custom Vue component) 20 | `scrolling-edge-size` | Number (`undefined`px) | When dragging a Drag component to the edge of this DropList, the pixel amount defines how close to the edge of the DropList a scroll will be triggered up/down/left/right (`0` = no scrolling on this DropList). `Undefined` default value means that this DropList will use whatever `scrolling-edge-size` is defined on the Drag component. 21 | 22 | ## Slots 23 | Slot Name | Description 24 | ---------- | ----------- 25 | `default` | Default content to add at the end of the DropList. Make sure to define a `key` prop for each element directly inside this slot. 26 | `item` | Used to render each list item. It has three properties, `item` , `index` and `reorder`. Reorder is true when the item is the one subject to reordering. **Don't forget to provide a key for the content of this slot !!** 27 | `feedback` | Used to render a placeholder to show the position where the new item would be inserted if the drag operation ended at the current mouse position. It has two properties : `type` and `data`. **Don't forget to provide a key for the content of this slot !!** 28 | `reordering-drag-image` | Defines the drag image to be used when reordering the list (Refer to **Drag Image** section above). 29 | `reordering-feedback` | Used to control the feedback used during reordering
* If this slot isn't defined, then the items switch positions during reordering to display in real time the order that will be achieved if the drag terminates at the current position
* If this slot is defined, then its content is inserted into the list to display the new location of the item being dragged (for an example of this, see nested drop lists) 30 | `empty` | Defined content to display if the list is empty and not being dragged into. Make sure to define a `key` prop for each element directly inside this slot. 31 | 32 | ## Demo 33 | https://codesandbox.io/s/droplist-ozs8b 34 | 35 | ![demo](../img/vid9.gif) 36 | 37 | ## Nested DropLists 38 | Drop lists can be nested providing the following conditions are satisfied : 39 | 40 | * the `row` or `column` props must be defined to inform the drop list components of the direction the items are lining up (mandatory) 41 | * for lists that support reordering, the `reordering-feedback` slot must be defined (advisable) 42 | * both the `feedback` and `reordering-feedback` slots must take no space in the layout (for example, `flex: 0 0 0; align-self: strech; outline: 1px solid blue;`) (advisable) 43 | 44 | Example : 45 | 46 | https://codesandbox.io/p/sandbox/nested-drop-lists-forked-qjq5tl 47 | 48 | ![demo](../img/vid11.gif) 49 | -------------------------------------------------------------------------------- /docs/components/dropmask.md: -------------------------------------------------------------------------------- 1 | # DropMask 2 | The `drop-mask` component is meant to create an island insensitive to drag and rop on top of a Drop component. 3 | 4 | ## Props 5 | Prop Name | Type / Default | Description 6 | --------- | -------------- | ----------- 7 | `tag` | Any | This prop can be used to customize the root of the template, Can refer to a custom Vue component, including its props, slots and listeners. 8 | 9 | ## Demo 10 | https://codesandbox.io/s/example-1-gvwsw 11 | 12 | ![demo](../img/vid2.gif) 13 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | ## Structure 4 | All emit events carry the current state of the drag operation by means of the following properties : 5 | 6 | * `type` : the type of the data being transferred 7 | * `data` : the data being transferred 8 | * `position` : the current position of the mouse cursor 9 | * `top` : the foremost Drop component currently under the mouse cursor if any 10 | * `previousTop` : for dragenter and dragleave, the previous value of top if any 11 | * `source` : the Drag component where the drag originated 12 | * `success` : whether the drag completed successfully or not 13 | * `native` : the associated mouse event (or touch event). Can be mousedown/touchstart, mousemove/touchmove or mouseup/touchend. 14 | 15 |  \ 16 |   17 | # Mixins 18 | ## DragAwareMixin 19 | 20 | A mixin is available to make components sensitive to drag operations. It adds the following computed to components that incorporate it, reflecting the current state of the drag : 21 | 22 | * `dragInProgress` : true if a drag operation is in progress, false otherwise 23 | * `dragType` : the type of the current drag operation 24 | * `dragData` : the data of the current drag operation 25 | * `dragPosition` : the current position of the mouse relative to the document 26 | * `dragSource` : the Drag component from which the drag operation originated 27 | * `dragTop` : the foremost Drop component under the mouse if any 28 | 29 | The following demo displays information about the current drag operation when it is in progress : 30 | 31 | https://codesandbox.io/p/sandbox/example-5-forked-ph7969 32 | 33 | ![demo](img/vid6.gif) 34 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ### Does it support touch devices? 4 | 5 | Yes, including tap `vibration` prop on Drag component. 6 | 7 | ### Does it support SSR? 8 | 9 | Yes. 10 | 11 | ### Does it support keyboard events? 12 | 13 | Yes. ESC key can be pressed to cancel the drag. 14 | 15 | 16 | ### Can this be used with Nuxt? 17 | 18 | Yes. 19 | 20 |  \ 21 |   22 | -------------------------------------------------------------------------------- /docs/img/vid1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid1.gif -------------------------------------------------------------------------------- /docs/img/vid10.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid10.gif -------------------------------------------------------------------------------- /docs/img/vid11.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid11.gif -------------------------------------------------------------------------------- /docs/img/vid12.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid12.gif -------------------------------------------------------------------------------- /docs/img/vid2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid2.gif -------------------------------------------------------------------------------- /docs/img/vid3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid3.gif -------------------------------------------------------------------------------- /docs/img/vid4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid4.gif -------------------------------------------------------------------------------- /docs/img/vid5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid5.gif -------------------------------------------------------------------------------- /docs/img/vid6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid6.gif -------------------------------------------------------------------------------- /docs/img/vid7.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid7.gif -------------------------------------------------------------------------------- /docs/img/vid8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid8.gif -------------------------------------------------------------------------------- /docs/img/vid9.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/docs/img/vid9.gif -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Vue-Easy-DnD" 7 | text: "Drag & Drop for Vue" 8 | tagline: A drag and drop implementation for Vue which replaces the *impossible* HTML5 drag and drop API 9 | actions: 10 | - theme: brand 11 | text: Get Started 12 | link: /installation 13 | - theme: alt 14 | text: Live Example 15 | link: https://codesandbox.io/s/droplist-ozs8b 16 | 17 | 18 | features: 19 | - title: Simple API 20 | details: With a simple API, you can create complex and scalable solutions. 21 | - title: Easy Installation 22 | details: Quick to install and supports SSR, Nuxt and Vue! 23 | - title: Easy to use 24 | details: Tools are very simple compared to other drop and drop packages. 25 | --- 26 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## About 4 | Easy-DnD is a drag and drop implementation for Vue that uses only standard mouse events instead of the HTML5 drag and drop API, which is [impossible to work with](https://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html). 5 | 6 | Think of it as a way to transfer data from some components to others using the mouse or support for a mouse assisted copy/cut - paste. It also allows for lists to be reordered by drag and drop. 7 | 8 | 9 | ## Installation 10 | 11 | Install via [npm](https://npmjs.com) or [yarn](https://yarnpkg.com) 12 | 13 | 14 | ### Vue 3 15 | 16 | ``` 17 | # Use npm 18 | npm install vue-easy-dnd@latest --save 19 | 20 | # Use yarn 21 | yarn add vue-easy-dnd@latest 22 | ``` 23 | 24 | #### Requirements 25 | 26 | 1. This package relies on the Options API and mixins. So make sure you have enabled the Options API in your project (enabled by default by Vue) 27 | 28 | 2. Make sure to import the generated CSS file: 29 | 30 | ```javascript 31 | import 'vue-easy-dnd/dist/dnd.css' 32 | ``` 33 | #### @vue/compat warning 34 | 35 | If you use @vue/compat, you may need to switch the MODE of our components 36 | 37 | More details about this issue can be found here https://github.com/rlemaigre/Easy-DnD/issues/145 38 | 39 | ```javascript 40 | DragList.compatConfig = { 41 | MODE: 3 42 | }; 43 | Drag.compatConfig = { 44 | MODE: 3 45 | }; 46 | ``` 47 | 48 | ### Vue 2 49 | The Vue2 variant is no longer maintained. Please use with caution. 50 | ``` 51 | # Use npm 52 | npm install vue-easy-dnd@^1 --save 53 | 54 | # Use yarn 55 | yarn add vue-easy-dnd@^1 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # Vue-Easy-DnD 2 | ### A HTML5 drag-and-drop replacement 3 | Easy-DnD is a drag and drop implementation for Vue that uses only standard mouse events instead of the HTML5 drag and drop API, which is [impossible to work with](https://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html). 4 | 5 | Think of it as a way to transfer data from some components to others using the mouse or support for a mouse assisted copy/cut - paste. It also allows for lists to be reordered by drag and drop. 6 | 7 | ## Documentation 8 | Our documentation has moved to Github Pages! 9 | 10 | ### [View Documentation Here](https://rlemaigre.github.io/Easy-DnD) 11 | 12 | ## Examples 13 | View more examples within our documentation 14 | 15 | ![demo](docs/img/vid4.gif) 16 | ![demo](docs/img/vid7.gif) 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-easy-dnd", 3 | "version": "2.2.2", 4 | "description": "Easy-DnD is a drag and drop implementation for Vue 3 that uses only standard mouse events instead of the HTML5 drag and drop API, which is [impossible to work with](https://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html). Think of it as a way to transfer data from some components to others using the mouse or support for a mouse assisted copy/cut - paste. It also allows for lists to be reordred by drag and drop.", 5 | "main": "./dist/vue-easy-dnd.ssr.js", 6 | "module": "./dist/vue-easy-dnd.esm.js", 7 | "browser": { 8 | "./dist/vue-easy-dnd.ssr.js": "./dist/vue-easy-dnd.js" 9 | }, 10 | "files": [ 11 | "dist", 12 | "src" 13 | ], 14 | "dependencies": { 15 | "mitt": "^3.0.1", 16 | "vue": "^3.0.0" 17 | }, 18 | "browserslist": [ 19 | "> 1%", 20 | "last 2 versions", 21 | "not ie <= 11" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/rlemaigre/Easy-DnD.git" 26 | }, 27 | "keywords": [ 28 | "vue", 29 | "vuejs", 30 | "vue2", 31 | "vuejs2", 32 | "vue3", 33 | "vuejs3", 34 | "drag-and-drop", 35 | "sortable", 36 | "dnd" 37 | ], 38 | "author": "Régis Lemaigre ", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/rlemaigre/Easy-DnD/issues" 42 | }, 43 | "homepage": "https://github.com/rlemaigre/Easy-DnD#readme", 44 | "engines": { 45 | "node": ">= 14" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import vue from 'rollup-plugin-vue'; 2 | import scss from 'rollup-plugin-scss'; 3 | import copy from 'rollup-plugin-cpy'; 4 | import del from 'rollup-plugin-delete' 5 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 6 | 7 | const buildName = 'vue-easy-dnd'; 8 | const input = 'src/index.js'; 9 | const external = ['vue']; 10 | const globals = { 11 | vue: 'vue' 12 | }; 13 | 14 | const plugins = [ 15 | del({ 16 | targets: 'dist/*', 17 | runOnce: true 18 | }), 19 | copy({ 20 | files: ['../README.md'], 21 | dest: 'dist/', 22 | copyOnce: true 23 | }), 24 | copy({ 25 | files: ['../README.md'], 26 | dest: '.', 27 | copyOnce: true 28 | }), 29 | scss({ 30 | fileName: 'dnd.css' 31 | }), 32 | nodeResolve() 33 | ]; 34 | 35 | export default [ 36 | // ESM build to be used with webpack/rollup 37 | { 38 | input, 39 | external, 40 | output: { 41 | format: 'esm', 42 | file: `dist/${buildName}.esm.js`, 43 | globals 44 | }, 45 | plugins: [ 46 | vue(), 47 | ...plugins 48 | ] 49 | }, 50 | // SSR build 51 | { 52 | input, 53 | external, 54 | output: { 55 | format: 'cjs', 56 | file: `dist/${buildName}.ssr.js`, 57 | globals 58 | }, 59 | plugins: [ 60 | vue({ template: { optimizeSSR: true } }), 61 | ...plugins 62 | ] 63 | }, 64 | // Browser build 65 | { 66 | input, 67 | external, 68 | output: { 69 | format: 'iife', 70 | name: 'VueEasyDnD', 71 | file: `dist/${buildName}.js`, 72 | globals 73 | }, 74 | plugins: [ 75 | vue(), 76 | ...plugins 77 | ] 78 | } 79 | ] 80 | -------------------------------------------------------------------------------- /lib/src/components/Drag.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 44 | 45 | 68 | 69 | 78 | -------------------------------------------------------------------------------- /lib/src/components/DragFeedback.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /lib/src/components/Drop.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 48 | 49 | 62 | 63 | 72 | -------------------------------------------------------------------------------- /lib/src/components/DropList.vue: -------------------------------------------------------------------------------- 1 | 436 | 437 | 456 | 457 | 468 | -------------------------------------------------------------------------------- /lib/src/components/DropMask.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 43 | -------------------------------------------------------------------------------- /lib/src/helpers/edgescroller.js: -------------------------------------------------------------------------------- 1 | // Forked from https://github.com/bennadel/JavaScript-Demos/blob/master/demos/window-edge-scrolling/index.htm 2 | // Code was altered to work with scrollable containers 3 | let timer = null; 4 | 5 | export function cancelScrollAction () { 6 | clearTimeout(timer); 7 | } 8 | 9 | function isBodyContainer (container) { 10 | return container === document.body; 11 | } 12 | 13 | // Determine if user is inside an edge of the container 14 | function isInEdge (container, clientX, clientY, edgeSize) { 15 | // Get the viewport-relative coordinates of the mousemove event. 16 | const rect = container.getBoundingClientRect(); 17 | const isBody = isBodyContainer(container); 18 | 19 | let viewportX = clientX - rect.left; 20 | let viewportY = clientY - rect.top; 21 | if (isBody) { 22 | viewportX = clientX; 23 | viewportY = clientY; 24 | } 25 | 26 | // Get the viewport dimensions. 27 | let viewportWidth = rect.width; 28 | let viewportHeight = rect.height; 29 | if (isBody) { 30 | viewportWidth = document.documentElement.clientWidth; 31 | viewportHeight = document.documentElement.clientHeight; 32 | } 33 | 34 | // Next, we need to determine if the mouse is within the "edge" of the 35 | // viewport, which may require scrolling the window. To do this, we need to 36 | // calculate the boundaries of the edge in the viewport (these coordinates 37 | // are relative to the viewport grid system). 38 | const edgeTop = edgeSize; 39 | const edgeLeft = edgeSize; 40 | const edgeBottom = ( viewportHeight - edgeSize ); 41 | const edgeRight = (viewportWidth - edgeSize); 42 | 43 | const isInLeftEdge = ( viewportX < edgeLeft ); 44 | const isInRightEdge = ( viewportX > edgeRight ); 45 | const isInTopEdge = ( viewportY < edgeTop ); 46 | const isInBottomEdge = ( viewportY > edgeBottom ); 47 | 48 | if (!(isInLeftEdge || isInRightEdge || isInTopEdge || isInBottomEdge)) { 49 | return null; 50 | } 51 | 52 | return { 53 | rect, 54 | viewportX, 55 | viewportY, 56 | viewportWidth, 57 | viewportHeight, 58 | isInLeftEdge, 59 | isInTopEdge, 60 | isInRightEdge, 61 | isInBottomEdge, 62 | edgeTop, 63 | edgeLeft, 64 | edgeBottom, 65 | edgeRight 66 | }; 67 | } 68 | 69 | // Determine if the scroll container has offets which will allow it to be scrolled X or Y 70 | function canContainerBeScrolled (container, viewportWidth, viewportHeight) { 71 | const isBody = isBodyContainer(container); 72 | 73 | // Get the document dimensions. 74 | const documentWidth = Math.max( 75 | container.scrollWidth, 76 | container.offsetWidth, 77 | container.clientWidth 78 | ); 79 | const documentHeight = Math.max( 80 | container.scrollHeight, 81 | container.offsetHeight, 82 | container.clientHeight 83 | ); 84 | 85 | // Calculate the maximum scroll offset in each direction. Since you can only 86 | // scroll the overflow portion of the document, the maximum represents the 87 | // length of the document that is NOT in the viewport. 88 | const maxScrollX = (documentWidth - viewportWidth); 89 | const maxScrollY = (documentHeight - viewportHeight); 90 | 91 | // Get the current scroll position of the document. 92 | let currentScrollX = container.scrollLeft; 93 | let currentScrollY = container.scrollTop; 94 | if (isBody) { 95 | currentScrollX = window.scrollX; 96 | currentScrollY = window.scrollY; 97 | } 98 | 99 | // Determine if the window can be scrolled in any particular direction. 100 | const canScrollUp = (currentScrollY > 0); 101 | const canScrollDown = (currentScrollY < maxScrollY); 102 | const canScrollLeft = (currentScrollX > 0); 103 | const canScrollRight = (currentScrollX < maxScrollX); 104 | 105 | return { 106 | documentWidth, 107 | documentHeight, 108 | maxScrollX, 109 | maxScrollY, 110 | currentScrollX, 111 | currentScrollY, 112 | canScroll: (canScrollUp || canScrollDown || canScrollLeft || canScrollRight), 113 | canScrollUp, 114 | canScrollDown, 115 | canScrollLeft, 116 | canScrollRight 117 | }; 118 | } 119 | 120 | // Determine whether the user is able to scroll based on current edge position and scroll of the container 121 | function canBeScrolledInCurrentDirection (container, edgeSize, edgeParams, scrollParams) { 122 | const { 123 | viewportX, 124 | viewportY, 125 | isInLeftEdge, 126 | isInRightEdge, 127 | isInTopEdge, 128 | isInBottomEdge, 129 | edgeTop, 130 | edgeLeft, 131 | edgeBottom, 132 | edgeRight 133 | } = edgeParams; 134 | 135 | const { 136 | maxScrollX, 137 | maxScrollY, 138 | currentScrollX, 139 | currentScrollY, 140 | canScrollUp, 141 | canScrollLeft, 142 | canScrollDown, 143 | canScrollRight 144 | } = scrollParams; 145 | 146 | // Since we can potentially scroll in two directions at the same time, 147 | // let's keep track of the next scroll, starting with the current scroll. 148 | // Each of these values can then be adjusted independently in the logic 149 | // below. 150 | let nextScrollX = currentScrollX; 151 | let nextScrollY = currentScrollY; 152 | 153 | // As we examine the mouse position within the edge, we want to make the 154 | // incremental scroll changes more "intense" the closer that the user 155 | // gets the viewport edge. As such, we'll calculate the percentage that 156 | // the user has made it "through the edge" when calculating the delta. 157 | // Then, that use that percentage to back-off from the "max" step value. 158 | const maxStep = 50; 159 | 160 | // Should we scroll left? 161 | if (isInLeftEdge && canScrollLeft) { 162 | const intensity = ((edgeLeft - viewportX) / edgeSize); 163 | nextScrollX = (nextScrollX - (maxStep * intensity)); 164 | } 165 | // Should we scroll right? 166 | else if (isInRightEdge && canScrollRight) { 167 | const intensity = ((viewportX - edgeRight) / edgeSize); 168 | nextScrollX = (nextScrollX + (maxStep * intensity)); 169 | } 170 | 171 | // Should we scroll up? 172 | if (isInTopEdge && canScrollUp) { 173 | const intensity = ((edgeTop - viewportY) / edgeSize); 174 | nextScrollY = (nextScrollY - (maxStep * intensity)); 175 | } 176 | // Should we scroll down? 177 | else if (isInBottomEdge && canScrollDown) { 178 | const intensity = ((viewportY - edgeBottom) / edgeSize); 179 | nextScrollY = (nextScrollY + (maxStep * intensity)); 180 | } 181 | 182 | // Sanitize invalid maximums. An invalid scroll offset won't break the 183 | // subsequent .scrollTo() call; however, it will make it harder to 184 | // determine if the .scrollTo() method should have been called in the 185 | // first place. 186 | nextScrollX = Math.max(0, Math.min(maxScrollX, nextScrollX)); 187 | nextScrollY = Math.max(0, Math.min(maxScrollY, nextScrollY)); 188 | 189 | if ((nextScrollX !== currentScrollX) || (nextScrollY !== currentScrollY)) { 190 | return { nextScrollX, nextScrollY }; 191 | } 192 | return null; 193 | } 194 | 195 | /** Main function to determine whether a node can be scrolled based on current cursor pos and container edge + scroll pos **/ 196 | export function isContainerReadyToEdgeScroll (container, clientX, clientY, edgeSize) { 197 | // Check that the user's cursor is currently within an edge of this scrollable container 198 | const edgeParams = isInEdge(container, clientX, clientY, edgeSize); 199 | if (!edgeParams) { 200 | return false; 201 | } 202 | 203 | // Check that the scrollable container still has remaining X or Y scroll 204 | const { viewportWidth, viewportHeight } = edgeParams; 205 | const scrollParams = canContainerBeScrolled(container, viewportWidth, viewportHeight); 206 | if (!scrollParams.canScroll) { 207 | return false; 208 | } 209 | 210 | // Check that the current cursor position sits within an edge and has remaining scroll to go 211 | const canScrollInCurrDirection = canBeScrolledInCurrentDirection(container, edgeSize, edgeParams, scrollParams); 212 | return !!canScrollInCurrDirection; 213 | } 214 | 215 | /** Main function for performing scroll action **/ 216 | export function performEdgeScroll (container, clientX, clientY, edgeSize) { 217 | if (!container || !edgeSize) { 218 | cancelScrollAction(); 219 | return false; 220 | } 221 | 222 | // NOTE: Much of the information here, with regard to document dimensions, 223 | // viewport dimensions, and window scrolling is derived from JavaScript.info. 224 | // I am consuming it here primarily as NOTE TO SELF. 225 | // -- 226 | // Read More: https://javascript.info/size-and-scroll-window 227 | // -- 228 | // CAUTION: The viewport and document dimensions can all be CACHED and then 229 | // recalculated on window-resize events (for the most part). I am keeping it 230 | // all here in the mousemove event handler to remove as many of the moving 231 | // parts as possible and keep the demo as simple as possible. 232 | 233 | // If the mouse is not in the viewport edge, there's no need to calculate 234 | // anything else. 235 | const edgeParams = isInEdge(container, clientX, clientY, edgeSize); 236 | if (!edgeParams) { 237 | cancelScrollAction(); 238 | return false; 239 | } 240 | 241 | const { viewportWidth, viewportHeight } = edgeParams; 242 | 243 | // If we made it this far, the user's mouse is located within the edge of the 244 | // viewport. As such, we need to check to see if scrolling needs to be done. 245 | 246 | // As we examine the mousemove event, we want to adjust the window scroll in 247 | // immediate response to the event; but, we also want to continue adjusting 248 | // the window scroll if the user rests their mouse in the edge boundary. To 249 | // do this, we'll invoke the adjustment logic immediately. Then, we'll setup 250 | // a timer that continues to invoke the adjustment logic while the window can 251 | // still be scrolled in a particular direction. 252 | (function checkForWindowScroll () { 253 | cancelScrollAction(); 254 | 255 | if (adjustWindowScroll()) { 256 | timer = setTimeout( checkForWindowScroll, 30 ); 257 | } 258 | })(); 259 | 260 | // Adjust the window scroll based on the user's mouse position. Returns True 261 | // or False depending on whether or not the window scroll was changed. 262 | function adjustWindowScroll () { 263 | const scrollParams = canContainerBeScrolled(container, viewportWidth, viewportHeight); 264 | 265 | const nextScrollParams = canBeScrolledInCurrentDirection(container, edgeSize, edgeParams, scrollParams); 266 | if (nextScrollParams) { 267 | const { nextScrollX, nextScrollY } = nextScrollParams; 268 | (isBodyContainer(container) ? window : container).scrollTo(nextScrollX, nextScrollY); 269 | return true; 270 | } 271 | 272 | return false; 273 | } 274 | 275 | return true; 276 | } 277 | -------------------------------------------------------------------------------- /lib/src/helpers/scrollparent.js: -------------------------------------------------------------------------------- 1 | // Forked from https://gist.github.com/gre/296291b8ce0d8fe6e1c3ea4f1d1c5c3b 2 | const regex = /(auto|scroll)/; 3 | 4 | const style = (node, prop) => 5 | getComputedStyle(node, null).getPropertyValue(prop); 6 | 7 | const scroll = (node) => 8 | regex.test( 9 | style(node, 'overflow') + 10 | style(node, 'overflow-y') + 11 | style(node, 'overflow-x')); 12 | 13 | const scrollparent = (node) => { 14 | if (!node || node === document.body) { 15 | return document.body; 16 | } 17 | 18 | if (scroll(node)) { 19 | return node; 20 | } 21 | 22 | return scrollparent(node.parentNode); 23 | }; 24 | 25 | export default scrollparent; 26 | -------------------------------------------------------------------------------- /lib/src/index.js: -------------------------------------------------------------------------------- 1 | import Drag from './components/Drag.vue'; 2 | import Drop from './components/Drop.vue'; 3 | import DropMask from './components/DropMask.vue'; 4 | import DropList from './components/DropList.vue'; 5 | import DragFeedback from './components/DragFeedback.vue'; 6 | 7 | import DragAwareMixin from './mixins/DragAwareMixin'; 8 | import DragMixin from './mixins/DragMixin'; 9 | import DropMixin from './mixins/DropMixin'; 10 | 11 | import { dnd } from './js/DnD'; 12 | import { DragImagesManager } from './js/DragImagesManager'; 13 | import { DnDEvent, InsertEvent, ReorderEvent } from './js/events'; 14 | import { createDragImage } from './js/createDragImage'; 15 | 16 | export { 17 | Drag, 18 | Drop, 19 | DropList, 20 | DropMask, 21 | DragFeedback, 22 | DragAwareMixin, 23 | DragMixin, 24 | DropMixin, 25 | DragImagesManager, 26 | dnd, 27 | DnDEvent, 28 | InsertEvent, 29 | ReorderEvent, 30 | createDragImage 31 | }; 32 | -------------------------------------------------------------------------------- /lib/src/js/DnD.js: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | import mitt from 'mitt'; 3 | 4 | /** 5 | * This is the class of the global object that holds the state of the drag and drop during its progress. It emits events 6 | * reporting its state evolution during the progress of the drag and drop. Its data is reactive and listeners can be 7 | * attached to it using the method on. 8 | */ 9 | export class DnD { 10 | 11 | inProgress = false; 12 | type = null; 13 | data = null; 14 | source = null; 15 | top = null; 16 | position = null; 17 | eventBus = mitt(); 18 | success = null; 19 | 20 | startDrag (source, event, x, y, type, data) { 21 | this.type = type; 22 | this.data = data; 23 | this.source = source; 24 | this.position = { x, y }; 25 | this.top = null; 26 | this.inProgress = true; 27 | this.emit(event, 'dragstart'); 28 | this.emit(event, 'dragtopchanged', { previousTop: null }); 29 | } 30 | 31 | resetVariables () { 32 | this.inProgress = false; 33 | this.data = null; 34 | this.source = null; 35 | this.position = null; 36 | this.success = null; 37 | } 38 | 39 | stopDrag (event) { 40 | this.success = this.top !== null && this.top['compatibleMode'] && this.top['dropAllowed']; 41 | if (this.top !== null) { 42 | this.emit(event, 'drop'); 43 | } 44 | this.emit(event, 'dragend'); 45 | this.resetVariables(); 46 | } 47 | 48 | cancelDrag (event) { 49 | this.success = false; 50 | this.emit(event, 'dragend'); 51 | this.resetVariables(); 52 | } 53 | 54 | mouseMove (event, comp) { 55 | if (this.inProgress) { 56 | let prevent = false; 57 | const previousTop = this.top; 58 | if (comp === null) { 59 | // The mouse move event reached the top of the document without hitting a drop component. 60 | this.top = null; 61 | prevent = true; 62 | } 63 | else if (comp['isDropMask']) { 64 | // The mouse move event bubbled until it reached a drop mask. 65 | this.top = null; 66 | prevent = true; 67 | } 68 | else if (comp['candidate'](this.type, this.data, this.source)) { 69 | // The mouse move event bubbled until it reached a drop component that participates in the current drag operation. 70 | this.top = comp; 71 | prevent = true; 72 | } 73 | 74 | if (prevent) { 75 | // We prevent the mouse move event from bubbling further up the tree because it reached the foremost drop component and that component is all that matters. 76 | event.stopPropagation(); 77 | } 78 | if (this.top !== previousTop) { 79 | this.emit(event.detail.native, 'dragtopchanged', { previousTop: previousTop }); 80 | } 81 | this.position = { 82 | x: event.detail.x, 83 | y: event.detail.y 84 | }; 85 | this.emit(event.detail.native, 'dragpositionchanged'); 86 | } 87 | } 88 | 89 | emit (native, event, data = {}) { 90 | this.eventBus.emit(event, { 91 | type: this.type, 92 | data: this.data, 93 | top: this.top, 94 | source: this.source, 95 | position: this.position, 96 | success: this.success, 97 | native, 98 | ...data 99 | }); 100 | } 101 | 102 | on (event, callback) { 103 | this.eventBus.on(event, callback); 104 | } 105 | 106 | off (event, callback) { 107 | this.eventBus.off(event, callback); 108 | } 109 | } 110 | 111 | export const dnd = reactive(new DnD()); 112 | -------------------------------------------------------------------------------- /lib/src/js/DragImagesManager.js: -------------------------------------------------------------------------------- 1 | import { dnd } from './DnD'; 2 | import { nextTick } from 'vue'; 3 | 4 | /** 5 | * This class reacts to drag events emitted by the dnd object to manage a sequence of drag images and fade from one to the 6 | * other as the drag progresses. 7 | */ 8 | export class DragImagesManager { 9 | 10 | selfTransform = null; 11 | clones = null; 12 | source = null; 13 | sourcePos = null; 14 | sourceClone = null; 15 | 16 | constructor () { 17 | dnd.on('dragstart', this.onDragStart.bind(this)); 18 | dnd.on('dragtopchanged', this.onDragTopChanged.bind(this)); 19 | dnd.on('dragpositionchanged', this.onDragPositionChanged.bind(this)); 20 | dnd.on('dragend', this.onDragEnd.bind(this)); 21 | } 22 | 23 | onDragStart (event) { 24 | // If go-back=true and it is still animating while they attempt another drag, 25 | // it will bug out. Best to clean up any existing elements on the page before 26 | // attempting to start the next animation 27 | this.cleanUp(); 28 | 29 | this.sourcePos = { 30 | x: event.source.$el.getBoundingClientRect().left, 31 | y: event.source.$el.getBoundingClientRect().top 32 | }; 33 | this.selfTransform = 'translate(-' + (event.position.x - this.sourcePos.x) + 'px, -' + (event.position.y - this.sourcePos.y) + 'px)'; 34 | this.clones = new Map(); 35 | this.source = event.source; 36 | } 37 | 38 | onDragEnd (event) { 39 | nextTick() 40 | .then(() => { 41 | if (!event.success && this.source && this.source['goBack']) { 42 | // Restore the drag image that is active when hovering outside any drop zone : 43 | const img = this.switch(null); 44 | 45 | // Move it back to its original place : 46 | window.requestAnimationFrame(() => { 47 | img.style.transition = 'all 0.5s'; 48 | window.requestAnimationFrame(() => { 49 | img.style.left = this.sourcePos.x + 'px'; 50 | img.style.top = this.sourcePos.y + 'px'; 51 | img.style.transform = 'translate(0,0)'; 52 | const handler = () => { 53 | this.cleanUp(); 54 | img.removeEventListener('transitionend', handler); 55 | }; 56 | img.addEventListener('transitionend', handler); 57 | }); 58 | }); 59 | } 60 | else { 61 | this.cleanUp(); 62 | } 63 | }); 64 | } 65 | 66 | cleanUp () { 67 | if (this.clones) { 68 | this.clones.forEach((clone) => { 69 | if (clone.parentNode === document.body) { 70 | document.body.removeChild(clone); 71 | } 72 | }); 73 | } 74 | if (this.sourceClone !== null) { 75 | if (this.sourceClone.parentNode === document.body) { 76 | document.body.removeChild(this.sourceClone); 77 | } 78 | } 79 | this.selfTransform = null; 80 | this.clones = null; 81 | this.source = null; 82 | this.sourceClone = null; 83 | this.sourcePos = null; 84 | } 85 | 86 | onDragTopChanged (event) { 87 | this.switch(event.top); 88 | } 89 | 90 | switch (top) { 91 | this.clones.forEach(clone => { 92 | clone.style.opacity = '0'; 93 | }); 94 | if (this.sourceClone) { 95 | this.sourceClone.style.opacity = '0'; 96 | } 97 | 98 | let activeClone; 99 | if (top === null) { 100 | activeClone = this.getSourceClone(); 101 | } 102 | else { 103 | if (!this.clones.has(top)) { 104 | let clone = top['createDragImage'](this.selfTransform); 105 | if (clone === 'source') { 106 | clone = this.getSourceClone(); 107 | } 108 | else if (clone !== null) { 109 | clone.style.opacity = '0'; 110 | document.body.appendChild(clone); 111 | } 112 | this.clones.set(top, clone); 113 | } 114 | activeClone = this.clones.get(top); 115 | } 116 | 117 | if (activeClone !== null) { 118 | activeClone.offsetWidth; // Forces browser reflow 119 | activeClone.style.opacity = activeClone['__opacity']; 120 | activeClone.style.visibility = 'visible'; 121 | } 122 | 123 | return activeClone; 124 | } 125 | 126 | getSourceClone () { 127 | if (this.sourceClone === null) { 128 | this.sourceClone = this.source['createDragImage'](this.selfTransform); 129 | this.sourceClone.style.opacity = '0'; 130 | document.body.appendChild(this.sourceClone); 131 | } 132 | return this.sourceClone; 133 | } 134 | 135 | onDragPositionChanged () { 136 | this.clones.forEach((clone) => { 137 | clone.style.left = dnd.position.x + 'px'; 138 | clone.style.top = dnd.position.y + 'px'; 139 | }); 140 | if (this.sourceClone) { 141 | this.sourceClone.style.left = dnd.position.x + 'px'; 142 | this.sourceClone.style.top = dnd.position.y + 'px'; 143 | } 144 | } 145 | 146 | } 147 | 148 | new DragImagesManager(); 149 | -------------------------------------------------------------------------------- /lib/src/js/Grid.js: -------------------------------------------------------------------------------- 1 | export default class Grid { 2 | reference; 3 | referenceOriginalPosition; 4 | magnets = []; 5 | 6 | constructor (collection, upToIndex, direction, fromIndex) { 7 | this.reference = collection.item(0).parentNode; 8 | this.referenceOriginalPosition = { 9 | x: this.reference.getBoundingClientRect().left - this.reference.scrollLeft, 10 | y: this.reference.getBoundingClientRect().top - this.reference.scrollTop, 11 | }; 12 | let index = 0; 13 | for (const child of collection) { 14 | if (index > upToIndex) break; 15 | const rect = child.getBoundingClientRect(); 16 | const hasNestedDrop = child.classList.contains('dnd-drop') || child.getElementsByClassName('dnd-drop').length > 0; 17 | let horizontal = false; 18 | if (hasNestedDrop) { 19 | if (direction === 'auto') { 20 | // Auto mode not supported for now. Row or column must be defined explicitly if there are nested drop lists. 21 | throw 'Easy-DnD error : a drop list is missing one of these attributes : \'row\' or \'column\'.'; 22 | } 23 | else { 24 | horizontal = direction === 'row'; 25 | } 26 | } 27 | if (fromIndex === null) { 28 | // Inserting mode. 29 | this.magnets.push(hasNestedDrop ? this.before(rect, horizontal) : this.center(rect)); 30 | } 31 | else { 32 | // Reordering mode. 33 | this.magnets.push(hasNestedDrop ? ( 34 | fromIndex < index ? this.after : this.before 35 | )(rect, horizontal) : this.center(rect)); 36 | } 37 | // Debug : show magnets : 38 | //document.body.insertAdjacentHTML("beforeend", "
") 39 | index++; 40 | } 41 | } 42 | 43 | /** 44 | * Returns the center of the rectangle. 45 | */ 46 | center (rect) { 47 | return { 48 | x: rect.left + rect.width / 2, 49 | y: rect.top + rect.height / 2 50 | }; 51 | } 52 | 53 | /** 54 | * When horizontal is true / false, returns middle of the left / top side of the rectangle. 55 | */ 56 | before (rect, horizontal) { 57 | return horizontal ? { 58 | x: rect.left, 59 | y: rect.top + rect.height / 2 60 | } : { 61 | x: rect.left + rect.width / 2, 62 | y: rect.top 63 | }; 64 | } 65 | 66 | /** 67 | * When horizontal is true / false, returns middle of the right / bottom side of the rectangle. 68 | */ 69 | after (rect, horizontal) { 70 | return horizontal ? { 71 | x: rect.left + rect.width, 72 | y: rect.top + rect.height / 2 73 | } : { 74 | x: rect.left + rect.width / 2, 75 | y: rect.top + rect.height 76 | }; 77 | } 78 | 79 | /** 80 | * In case the user scrolls during the drag, the position of the magnets are not what they used to be when the drag 81 | * started. A correction must be applied that takes into account the amount of scroll. This correction is the 82 | * difference between the current position of the parent element and its position when the drag started. 83 | */ 84 | correction () { 85 | return { 86 | x: this.reference.getBoundingClientRect().left - this.reference.scrollLeft - this.referenceOriginalPosition.x, 87 | y: this.reference.getBoundingClientRect().top - this.reference.scrollTop - this.referenceOriginalPosition.y, 88 | }; 89 | } 90 | 91 | closestIndex (position) { 92 | const x = position.x - this.correction().x; 93 | const y = position.y - this.correction().y; 94 | let minDist = 999999; 95 | let index = -1; 96 | for (let i = 0; i < this.magnets.length; i++) { 97 | const magnet = this.magnets[i]; 98 | const dist = Math.sqrt(Math.pow(magnet.x - x, 2) + Math.pow(magnet.y - y, 2)); 99 | if (dist < minDist) { 100 | minDist = dist; 101 | index = i; 102 | } 103 | } 104 | return index; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/js/createDragImage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This files contains the primitives required to create drag images from HTML elements that serve as models. A snapshot 3 | * of the computed styles of the model elements is taken when creating the drag image, so that it will look the same as 4 | * the model, no matter where the drag images is grafted into the DOM. 5 | */ 6 | 7 | /** 8 | * Creates a drag image using the given element as model. 9 | */ 10 | export function createDragImage (el) { 11 | const clone = deepClone(el); 12 | clone.style.position = 'fixed'; 13 | clone.style.margin = '0'; 14 | clone.style['z-index'] = '1000'; 15 | clone.style.transition = 'opacity 0.2s'; 16 | return clone; 17 | } 18 | 19 | /** 20 | * Clones the given element and all its descendants. 21 | */ 22 | function deepClone (el) { 23 | const clone = el.cloneNode(true); 24 | copyStyle(el, clone); 25 | const vSrcElements = el.getElementsByTagName('*'); 26 | const vDstElements = clone.getElementsByTagName('*'); 27 | for (let i = vSrcElements.length; i--;) { 28 | const vSrcElement = vSrcElements[i]; 29 | const vDstElement = vDstElements[i]; 30 | copyStyle(vSrcElement, vDstElement); 31 | } 32 | return clone; 33 | } 34 | 35 | /** 36 | * Copy the computed styles from src to destination. 37 | */ 38 | function copyStyle (src, destination) { 39 | const computedStyle = window.getComputedStyle(src); 40 | for (const key of computedStyle) { 41 | destination.style.setProperty( 42 | key, 43 | computedStyle.getPropertyValue(key), 44 | computedStyle.getPropertyPriority(key) 45 | ); 46 | } 47 | destination.style.pointerEvents = 'none'; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/js/events.js: -------------------------------------------------------------------------------- 1 | export class DnDEvent { 2 | type; 3 | data; 4 | top; 5 | previousTop; 6 | source; 7 | position; 8 | success; 9 | native; 10 | } 11 | 12 | export class ReorderEvent { 13 | from; 14 | to; 15 | 16 | constructor (from, to) { 17 | this.from = from; 18 | this.to = to; 19 | } 20 | 21 | apply (array) { 22 | const temp = array[this.from]; 23 | array.splice(this.from, 1); 24 | array.splice(this.to, 0, temp); 25 | } 26 | 27 | } 28 | 29 | export class InsertEvent { 30 | type; 31 | data; 32 | index; 33 | 34 | constructor (type, data, index) { 35 | this.type = type; 36 | this.data = data; 37 | this.index = index; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/mixins/DragAwareMixin.js: -------------------------------------------------------------------------------- 1 | import { dnd } from '../js/DnD'; 2 | 3 | export default { 4 | data () { 5 | return { 6 | isDropMask: false 7 | }; 8 | }, 9 | computed: { 10 | dragInProgress () { 11 | return dnd.inProgress; 12 | }, 13 | dragData () { 14 | return dnd.data; 15 | }, 16 | dragType () { 17 | return dnd.type; 18 | }, 19 | dragPosition () { 20 | return dnd.position; 21 | }, 22 | dragSource () { 23 | return dnd.source; 24 | }, 25 | dragTop () { 26 | return dnd.top; 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /lib/src/mixins/DragMixin.js: -------------------------------------------------------------------------------- 1 | import DragAwareMixin from './DragAwareMixin'; 2 | import { createDragImage } from '../js/createDragImage'; 3 | import { dnd } from '../js/DnD'; 4 | import scrollparent from '../helpers/scrollparent'; 5 | import { cancelScrollAction, performEdgeScroll, isContainerReadyToEdgeScroll } from '../helpers/edgescroller'; 6 | 7 | export default { 8 | mixins: [DragAwareMixin], 9 | props: { 10 | type: { 11 | type: String, 12 | default: null 13 | }, 14 | data: { 15 | default: null 16 | }, 17 | dragImageOpacity: { 18 | type: Number, 19 | default: 0.7 20 | }, 21 | disabled: { 22 | type: Boolean, 23 | default: false 24 | }, 25 | goBack: { 26 | type: Boolean, 27 | default: false 28 | }, 29 | handle: { 30 | type: String, 31 | default: null 32 | }, 33 | delta: { 34 | type: Number, 35 | default: 0 36 | }, 37 | delay: { 38 | type: Number, 39 | default: 0 40 | }, 41 | dragClass: { 42 | type: String, 43 | default: null 44 | }, 45 | vibration: { 46 | type: Number, 47 | default: 0 48 | }, 49 | scrollingEdgeSize: { 50 | type: Number, 51 | default: 100 52 | } 53 | }, 54 | emits: ['dragstart', 'dragend', 'cut', 'copy'], 55 | data () { 56 | return { 57 | dragInitialised: false, 58 | dragStarted: false, 59 | ignoreNextClick: false, 60 | initialUserSelect: null, 61 | downEvent: null, 62 | startPosition: null, 63 | delayTimer: null, 64 | scrollContainer: null 65 | }; 66 | }, 67 | computed: { 68 | cssClasses () { 69 | const clazz = { 70 | 'dnd-drag': true 71 | }; 72 | if (!this.disabled) { 73 | return { 74 | ...clazz, 75 | 'drag-source': this.dragInProgress && this.dragSource === this, 76 | 'drag-mode-copy': this.currentDropMode === 'copy', 77 | 'drag-mode-cut': this.currentDropMode === 'cut', 78 | 'drag-mode-reordering': this.currentDropMode === 'reordering', 79 | 'drag-no-handle': !this.handle 80 | }; 81 | } 82 | else { 83 | return clazz; 84 | } 85 | }, 86 | currentDropMode () { 87 | if (this.dragInProgress && this.dragSource === this) { 88 | if (this.dragTop && this.dragTop['dropAllowed']) { 89 | if (this.dragTop['reordering']) { 90 | return 'reordering'; 91 | } 92 | else { 93 | return this.dragTop['mode']; 94 | } 95 | } 96 | else { 97 | return null; 98 | } 99 | } 100 | else { 101 | return null; 102 | } 103 | } 104 | }, 105 | methods: { 106 | onSelectStart (e) { 107 | e.stopPropagation(); 108 | e.preventDefault(); 109 | }, 110 | performVibration () { 111 | // If browser can perform vibration and user has defined a vibration, perform it 112 | if (this.vibration > 0 && window.navigator && window.navigator.vibrate) { 113 | window.navigator.vibrate(this.vibration); 114 | } 115 | }, 116 | onMouseDown (e) { 117 | let target = null; 118 | let goodButton = false; 119 | if (e.type === 'mousedown') { 120 | const mouse = e; 121 | target = e.target; 122 | goodButton = mouse.buttons === 1; 123 | } 124 | else { 125 | const touch = e; 126 | target = touch.touches[0].target; 127 | goodButton = true; 128 | } 129 | 130 | if (this.disabled || this.downEvent !== null || !goodButton) { 131 | return; 132 | } 133 | 134 | // Check that the target element is eligible for starting a drag 135 | // Includes checking against the handle selector 136 | // or whether the element contains 'dnd-no-drag' class (which should disable dragging from that 137 | // sub-element of a draggable parent) 138 | const goodTarget = !target.matches('.dnd-no-drag, .dnd-no-drag *') && 139 | ( 140 | !this.handle || 141 | target.matches(this.handle + ', ' + this.handle + ' *') 142 | ); 143 | 144 | if (!goodTarget) { 145 | return; 146 | } 147 | 148 | this.scrollContainer = scrollparent(target); 149 | this.initialUserSelect = document.body.style.userSelect; 150 | document.documentElement.style.userSelect = 'none'; // Permet au drag de se poursuivre normalement même 151 | // quand on quitte un élémént avec overflow: hidden. 152 | this.dragStarted = false; 153 | this.downEvent = e; 154 | if (this.downEvent.type === 'mousedown') { 155 | const mouse = e; 156 | this.startPosition = { 157 | x: mouse.clientX, 158 | y: mouse.clientY 159 | }; 160 | } 161 | else { 162 | const touch = e; 163 | this.startPosition = { 164 | x: touch.touches[0].clientX, 165 | y: touch.touches[0].clientY 166 | }; 167 | } 168 | 169 | if (this.delay) { 170 | this.dragInitialised = false; 171 | clearTimeout(this.delayTimer); 172 | this.delayTimer = setTimeout(() => { 173 | this.dragInitialised = true; 174 | this.performVibration(); 175 | }, this.delay); 176 | } 177 | else { 178 | this.dragInitialised = true; 179 | this.performVibration(); 180 | } 181 | 182 | document.addEventListener('click', this.onMouseClick, true); 183 | document.addEventListener('mouseup', this.onMouseUp); 184 | document.addEventListener('touchend', this.onMouseUp); 185 | document.addEventListener('selectstart', this.onSelectStart); 186 | document.addEventListener('keyup', this.onKeyUp); 187 | 188 | setTimeout(() => { 189 | document.addEventListener('mousemove', this.onMouseMove); 190 | document.addEventListener('touchmove', this.onMouseMove, { passive: false }); 191 | document.addEventListener('easy-dnd-move', this.onEasyDnDMove); 192 | }, 0); 193 | 194 | // Prevents event from bubbling to ancestor drag components and initiate several drags at the same time 195 | e.stopPropagation(); 196 | }, 197 | // Prevent the user from accidentally causing a click event 198 | // if they have just attempted a drag event 199 | onMouseClick (e) { 200 | if (this.ignoreNextClick) { 201 | e.preventDefault(); 202 | e.stopPropagation && e.stopPropagation(); 203 | e.stopImmediatePropagation && e.stopImmediatePropagation(); 204 | this.ignoreNextClick = false; 205 | return false; 206 | } 207 | }, 208 | onMouseMove (e) { 209 | // We ignore the mousemove event that follows touchend : 210 | if (this.downEvent === null) return; 211 | 212 | // On touch devices, we ignore fake mouse events and deal with touch events only. 213 | if (this.downEvent.type === 'touchstart' && e.type === 'mousemove') return; 214 | 215 | // Find out event target and pointer position : 216 | let target = null; 217 | let x = null; 218 | let y = null; 219 | if (e.type === 'touchmove') { 220 | const touch = e; 221 | x = touch.touches[0].clientX; 222 | y = touch.touches[0].clientY; 223 | target = document.elementFromPoint(x, y); 224 | if (!target) { 225 | // Mouse going off screen. Ignore event. 226 | return; 227 | } 228 | } 229 | else { 230 | const mouse = e; 231 | x = mouse.clientX; 232 | y = mouse.clientY; 233 | target = mouse.target; 234 | } 235 | 236 | // Distance between current event and start position : 237 | const dist = Math.sqrt(Math.pow(this.startPosition.x - x, 2) + Math.pow(this.startPosition.y - y, 2)); 238 | 239 | // If the drag has not begun yet and distance from initial point is greater than delta, we start the drag : 240 | if (!this.dragStarted && dist > this.delta) { 241 | // If they have dragged greater than the delta before the delay period has ended, 242 | // It means that they attempted to perform another action (such as scrolling) on the page 243 | if (!this.dragInitialised) { 244 | clearTimeout(this.delayTimer); 245 | } 246 | else { 247 | this.ignoreNextClick = true; 248 | this.dragStarted = true; 249 | dnd.startDrag(this, this.downEvent, this.startPosition.x, this.startPosition.y, this.type, this.data); 250 | document.documentElement.classList.add('drag-in-progress'); 251 | } 252 | } 253 | 254 | // Dispatch custom easy-dnd-move event : 255 | if (this.dragStarted) { 256 | // If cursor/touch is at edge of container, perform scroll if available 257 | // If this.dragTop is defined, it means they are dragging on top of another DropList/EasyDnd component 258 | // if dropTop is a DropList, use the scrollingEdgeSize of that container if it exists, otherwise use the scrollingEdgeSize of the Drag component 259 | const currEdgeSize = this.dragTop && this.dragTop.$props.scrollingEdgeSize !== undefined ? 260 | this.dragTop.$props.scrollingEdgeSize : 261 | this.scrollingEdgeSize; 262 | 263 | if (currEdgeSize) { 264 | // Create an array of all scrollable elements going upward until the body is hit 265 | let currScrollContainer = this.dragTop ? scrollparent(this.dragTop.$el) : this.scrollContainer; 266 | const nodes = [currScrollContainer]; 267 | do { 268 | if (currScrollContainer === document.body) { 269 | break; 270 | } 271 | 272 | currScrollContainer = scrollparent(currScrollContainer.parentNode); 273 | if (!currScrollContainer || currScrollContainer === document.body) { 274 | break; 275 | } 276 | nodes.push(currScrollContainer); 277 | } while (currScrollContainer && currScrollContainer !== document.body); 278 | 279 | // Iterate through all these nodes starting from the closest to body, and work towards current node 280 | for (let i = nodes.length - 1; i >= 0; i--) { 281 | cancelScrollAction(); 282 | const thisNode = nodes[i]; 283 | 284 | // Check that the current cursor pos + edge of container + scroll pos of container allows user 285 | // to start/continue scrolling in current direction 286 | if (isContainerReadyToEdgeScroll(thisNode, x, y, currEdgeSize)) { 287 | performEdgeScroll(thisNode, x, y, currEdgeSize); 288 | break; 289 | } 290 | } 291 | } 292 | else { 293 | cancelScrollAction(); 294 | } 295 | 296 | const custom = new CustomEvent('easy-dnd-move', { 297 | bubbles: true, 298 | cancelable: true, 299 | detail: { 300 | x, 301 | y, 302 | native: e 303 | } 304 | }); 305 | target.dispatchEvent(custom); 306 | } 307 | 308 | // Prevent scroll on touch devices if they were performing a drag 309 | if (this.dragInitialised && e.cancelable) { 310 | e.preventDefault(); 311 | } 312 | }, 313 | onEasyDnDMove (e) { 314 | dnd.mouseMove(e, null); 315 | }, 316 | onMouseUp (e) { 317 | // On touch devices, we ignore fake mouse events and deal with touch events only. 318 | if (this.downEvent.type === 'touchstart' && e.type === 'mouseup') return; 319 | 320 | // This delay makes sure that when the click event that results from the mouseup is produced, the drag is 321 | // still in progress. So by checking the flag dnd.inProgress, one can tell apart true clicks from drag and 322 | // drop artefacts. 323 | setTimeout(() => { 324 | this.cancelDragActions(); 325 | 326 | if (this.dragStarted) { 327 | dnd.stopDrag(e); 328 | } 329 | this.finishDrag(); 330 | }, 0); 331 | }, 332 | onKeyUp (e) { 333 | // If ESC is pressed, cancel the drag 334 | if (e.key === 'Escape') { 335 | this.cancelDragActions(); 336 | 337 | setTimeout(() => { 338 | dnd.cancelDrag(e); 339 | this.finishDrag(); 340 | }, 0); 341 | } 342 | }, 343 | cancelDragActions () { 344 | this.dragInitialised = false; 345 | clearTimeout(this.delayTimer); 346 | cancelScrollAction(); 347 | }, 348 | finishDrag () { 349 | this.downEvent = null; 350 | this.scrollContainer = null; 351 | 352 | if (this.dragStarted) { 353 | document.documentElement.classList.remove('drag-in-progress'); 354 | } 355 | document.removeEventListener('click', this.onMouseClick, true); 356 | document.removeEventListener('mousemove', this.onMouseMove); 357 | document.removeEventListener('touchmove', this.onMouseMove); 358 | document.removeEventListener('easy-dnd-move', this.onEasyDnDMove); 359 | document.removeEventListener('mouseup', this.onMouseUp); 360 | document.removeEventListener('touchend', this.onMouseUp); 361 | document.removeEventListener('selectstart', this.onSelectStart); 362 | document.removeEventListener('keyup', this.onKeyUp); 363 | document.documentElement.style.userSelect = this.initialUserSelect; 364 | }, 365 | dndDragStart (ev) { 366 | if (ev.source === this) { 367 | this.$emit('dragstart', ev); 368 | } 369 | }, 370 | dndDragEnd (ev) { 371 | if (ev.source === this) { 372 | this.$emit('dragend', ev); 373 | } 374 | }, 375 | createDragImage (selfTransform) { 376 | let image; 377 | if (this.$slots['drag-image']) { 378 | const el = this.$refs['drag-image'] || document.createElement('div'); 379 | if (el.childElementCount !== 1) { 380 | image = createDragImage(el); 381 | } 382 | else { 383 | image = createDragImage(el.children.item(0)); 384 | } 385 | } 386 | else { 387 | image = createDragImage(this.$el); 388 | image.style.transform = selfTransform; 389 | } 390 | 391 | if (this.dragClass) { 392 | image.classList.add(this.dragClass); 393 | } 394 | image.classList.add('dnd-ghost'); 395 | image['__opacity'] = this.dragImageOpacity; 396 | return image; 397 | } 398 | }, 399 | created () { 400 | dnd.on('dragstart', this.dndDragStart); 401 | dnd.on('dragend', this.dndDragEnd); 402 | }, 403 | mounted () { 404 | this.$el.addEventListener('mousedown', this.onMouseDown, { passive: true }); 405 | this.$el.addEventListener('touchstart', this.onMouseDown, { passive: true }); 406 | }, 407 | beforeUnmount () { 408 | dnd.off('dragstart', this.dndDragStart); 409 | dnd.off('dragend', this.dndDragEnd); 410 | 411 | this.$el.removeEventListener('mousedown', this.onMouseDown); 412 | this.$el.removeEventListener('touchstart', this.onMouseDown); 413 | } 414 | }; 415 | -------------------------------------------------------------------------------- /lib/src/mixins/DropMixin.js: -------------------------------------------------------------------------------- 1 | import DragAwareMixin from './DragAwareMixin'; 2 | import { createDragImage } from '../js/createDragImage'; 3 | import { dnd } from '../js/DnD'; 4 | 5 | export function dropAllowed (inst) { 6 | if (inst.dragInProgress && inst.typeAllowed) { 7 | return inst.compatibleMode && inst.effectiveAcceptsData(inst.dragData, inst.dragType); 8 | } 9 | return null; 10 | } 11 | 12 | export function doDrop (inst, event) { 13 | inst.$emit('drop', event); 14 | event.source.$emit(inst.mode, event); 15 | } 16 | 17 | export function candidate (inst, type) { 18 | return inst.effectiveAcceptsType(type); 19 | } 20 | 21 | export default { 22 | mixins: [DragAwareMixin], 23 | props: { 24 | acceptsType: { 25 | type: [String, Array, Function], 26 | default: null 27 | }, 28 | acceptsData: { 29 | type: Function, 30 | default: () => { 31 | return true; 32 | } 33 | }, 34 | mode: { 35 | type: String, 36 | default: 'copy' 37 | }, 38 | dragImageOpacity: { 39 | type: Number, 40 | default: 0.7 41 | } 42 | }, 43 | emits: ['dragover', 'dragenter', 'dragleave', 'dragend', 'drop'], 44 | data () { 45 | return { 46 | isDrop: true 47 | }; 48 | }, 49 | computed: { 50 | compatibleMode () { 51 | return this.dragInProgress ? true : null; 52 | }, 53 | dropIn () { 54 | if (this.dragInProgress) { 55 | return this.dragTop === this; 56 | } 57 | return null; 58 | }, 59 | typeAllowed () { 60 | if (this.dragInProgress) { 61 | return this.effectiveAcceptsType(this.dragType); 62 | } 63 | return null; 64 | }, 65 | dropAllowed () { 66 | return dropAllowed(this); 67 | }, 68 | cssClasses () { 69 | const clazz = { 70 | 'dnd-drop': true 71 | }; 72 | if (this.dropIn !== null) { 73 | clazz['drop-in'] = this.dropIn; 74 | clazz['drop-out'] = !this.dropIn; 75 | } 76 | if (this.typeAllowed !== null) { 77 | clazz['type-allowed'] = this.typeAllowed; 78 | clazz['type-forbidden'] = !this.typeAllowed; 79 | } 80 | if (this.dropAllowed !== null) { 81 | clazz['drop-allowed'] = this.dropAllowed; 82 | clazz['drop-forbidden'] = !this.dropAllowed; 83 | } 84 | return clazz; 85 | } 86 | }, 87 | methods: { 88 | effectiveAcceptsType (type) { 89 | if (this.acceptsType === null) { 90 | return true; 91 | } 92 | else if (typeof (this.acceptsType) === 'string' || typeof(this.acceptsType) === 'number') { 93 | return this.acceptsType === type; 94 | } 95 | else if (typeof (this.acceptsType) === 'object' && Array.isArray(this.acceptsType)) { 96 | return this.acceptsType.includes(type); 97 | } 98 | else { 99 | return this.acceptsType(type); 100 | } 101 | }, 102 | effectiveAcceptsData (data, type) { 103 | return this.acceptsData(data, type); 104 | }, 105 | onDragPositionChanged (event) { 106 | if (this === event.top) { 107 | this.$emit('dragover', event); 108 | } 109 | }, 110 | onDragTopChanged (event) { 111 | if (this === event.top) { 112 | this.$emit('dragenter', event); 113 | } 114 | if (this === event.previousTop) { 115 | this.$emit('dragleave', event); 116 | } 117 | }, 118 | onDragEnd (event) { 119 | if (this === event.top) { 120 | this.$emit('dragend', event); 121 | } 122 | }, 123 | onDrop (event) { 124 | if (this.dropIn && this.compatibleMode && this.dropAllowed) { 125 | this.doDrop(event); 126 | } 127 | }, 128 | doDrop (event) { 129 | doDrop(this, event); 130 | }, 131 | /** 132 | * Returns true if the current drop area participates in the current drag operation. 133 | */ 134 | candidate (type) { 135 | return candidate(this, type); 136 | }, 137 | createDragImage () { 138 | let image = 'source'; 139 | if (this.$refs['drag-image']) { 140 | const el = this.$refs['drag-image']; 141 | if (el.childElementCount !== 1) { 142 | image = createDragImage(el); 143 | } 144 | else { 145 | image = createDragImage(el.children.item(0)); 146 | } 147 | image['__opacity'] = this.dragImageOpacity; 148 | image.classList.add('dnd-ghost'); 149 | } 150 | return image; 151 | }, 152 | onDnDMove (e) { 153 | dnd.mouseMove(e, this); 154 | } 155 | }, 156 | created () { 157 | dnd.on('dragpositionchanged', this.onDragPositionChanged); 158 | dnd.on('dragtopchanged', this.onDragTopChanged); 159 | dnd.on('drop', this.onDrop); 160 | dnd.on('dragend', this.onDragEnd); 161 | }, 162 | mounted () { 163 | this.$el.addEventListener('easy-dnd-move', this.onDnDMove); 164 | }, 165 | beforeUnmount () { 166 | this.$el.removeEventListener('easy-dnd-move', this.onDnDMove); 167 | 168 | dnd.off('dragpositionchanged', this.onDragPositionChanged); 169 | dnd.off('dragtopchanged', this.onDragTopChanged); 170 | dnd.off('drop', this.onDrop); 171 | dnd.off('dragend', this.onDragEnd); 172 | } 173 | }; 174 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-easy-dnd", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build-dev": "cd ./lib && rollup --config rollup.config.mjs", 8 | "build": "cd ./lib && rollup --config rollup.config.mjs --environment BUILD:production", 9 | "publish": "cd ./lib && npm publish", 10 | "lint": "vue-cli-service lint ./src ./lib", 11 | "docs:dev": "vitepress dev docs", 12 | "docs:build": "vitepress build docs", 13 | "docs:preview": "vitepress preview docs" 14 | }, 15 | "dependencies": { 16 | "@babel/polyfill": "^7.12.1", 17 | "@segi/template": "file:lib", 18 | "core-js": "^3.41.0", 19 | "mitt": "^3.0.1", 20 | "vue": "^3.2.45" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.26.10", 24 | "@babel/eslint-parser": "^7.27.0", 25 | "@rollup/plugin-node-resolve": "^15.3.1", 26 | "@vue/cli-plugin-babel": "~5.0.0", 27 | "@vue/cli-plugin-eslint": "~5.0.0", 28 | "@vue/cli-service": "~5.0.0", 29 | "eslint": "^7.32.0", 30 | "eslint-plugin-css": "^0.6.0", 31 | "eslint-plugin-vue": "^8.7.1", 32 | "rollup": "^3.29.5", 33 | "rollup-plugin-cpy": "^2.0.1", 34 | "rollup-plugin-delete": "^2.2.0", 35 | "rollup-plugin-scss": "^4.0.1", 36 | "rollup-plugin-vue": "^6.0.0", 37 | "sass": "^1.86.3", 38 | "sass-loader": "^13.3.3", 39 | "vitepress": "^1.6.3" 40 | }, 41 | "eslintConfig": { 42 | "root": true, 43 | "env": { 44 | "node": true 45 | }, 46 | "extends": [ 47 | "plugin:vue/vue3-recommended", 48 | "eslint:recommended", 49 | "plugin:css/recommended" 50 | ], 51 | "parserOptions": { 52 | "parser": "@babel/eslint-parser" 53 | }, 54 | "rules": { 55 | "vue/multi-word-component-names": "off", 56 | "vue/no-unused-vars": "off", 57 | "no-prototype-builtins": "off", 58 | "vue/no-mutating-props": "off", 59 | "vue/max-attributes-per-line": [ 60 | "error", 61 | { 62 | "singleline": 2, 63 | "multiline": 1 64 | } 65 | ], 66 | "vue/html-self-closing": [ 67 | "error", 68 | { 69 | "html": { 70 | "void": "always", 71 | "normal": "always", 72 | "component": "always" 73 | }, 74 | "svg": "always", 75 | "math": "always" 76 | } 77 | ], 78 | "vue/no-v-html": "off", 79 | "no-multiple-empty-lines": "error", 80 | "semi": "error", 81 | "space-before-function-paren": "error", 82 | "prefer-const": "error", 83 | "object-curly-spacing": [ 84 | "error", 85 | "always" 86 | ], 87 | "quotes": [ 88 | "error", 89 | "single" 90 | ], 91 | "indent": [ 92 | "error", 93 | 2 94 | ], 95 | "brace-style": [ 96 | "error", 97 | "stroustrup", 98 | { 99 | "allowSingleLine": true 100 | } 101 | ] 102 | } 103 | }, 104 | "browserslist": [ 105 | "> 1%", 106 | "last 2 versions", 107 | "not dead", 108 | "not ie 11" 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlemaigre/Easy-DnD/ad4442120377e92839a91e02dff403d87c70a458/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | demo 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 103 | 104 | 109 | 110 | 233 | -------------------------------------------------------------------------------- /src/App10.vue: -------------------------------------------------------------------------------- 1 |