├── dev ├── .gitignore ├── public │ ├── favicon.png │ ├── index.html │ └── global.css ├── src │ ├── main.js │ ├── CloseIcon.svelte │ ├── SettingsIcon.svelte │ └── App.svelte ├── package.json ├── rollup.config.js ├── README.md └── yarn.lock ├── .prettierignore ├── src ├── index.js ├── useDropOutside.css ├── utils │ ├── resolveDragImage.js │ └── __tests__ │ │ └── resolveDragImage.test.js ├── useDropOutside.js ├── DragAndDrop.js └── __tests__ │ └── useDropOutside.test.js ├── .babelrc ├── .husky ├── commit-msg └── pre-commit ├── assets ├── container-switch.gif └── svelte-use-drop-outside.gif ├── commitlint.config.js ├── .gitignore ├── .github └── workflows │ ├── index.yml │ └── codeql.yml ├── .prettierrc ├── rollup.config.js ├── LICENSE ├── jest └── jest.setup.js ├── CHANGELOG.md ├── package.json └── README.md /dev/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .husky/ 2 | .idea/ 3 | build/ 4 | coverage/ 5 | node_modules/ -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as useDropOutside } from './useDropOutside' 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", {"targets": {"node": "current"}}]] 3 | } -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn test:ci 5 | yarn prettier 6 | -------------------------------------------------------------------------------- /dev/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/untemps/svelte-use-drop-outside/HEAD/dev/public/favicon.png -------------------------------------------------------------------------------- /assets/container-switch.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/untemps/svelte-use-drop-outside/HEAD/assets/container-switch.gif -------------------------------------------------------------------------------- /assets/svelte-use-drop-outside.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/untemps/svelte-use-drop-outside/HEAD/assets/svelte-use-drop-outside.gif -------------------------------------------------------------------------------- /dev/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte' 2 | 3 | const app = new App({ 4 | target: document.body, 5 | }) 6 | 7 | export default app 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'subject-case': [2, 'always', ['sentence-case']], 5 | 'scope-case': [2, 'always', ['lower-case', 'upper-case']], 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /src/useDropOutside.css: -------------------------------------------------------------------------------- 1 | .__drag { 2 | position: absolute; 3 | z-index: 999; 4 | user-select: none; 5 | opacity: .7; 6 | 7 | --origin-x: 0px; 8 | --origin-y: 0px; 9 | } 10 | 11 | @keyframes move { 12 | 100% { 13 | left: var(--origin-x); 14 | top: var(--origin-y); 15 | } 16 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | .cache/ 3 | dist/ 4 | stats.html 5 | 6 | # Dependencies 7 | node_modules/ 8 | 9 | # Misc 10 | .DS_STORE 11 | 12 | # NPM 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | package-lock.json 17 | .yarn 18 | 19 | # Webstorm 20 | .idea/ 21 | *.iml 22 | 23 | # Jest 24 | coverage/ 25 | __snapshots__/ -------------------------------------------------------------------------------- /dev/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte app 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/utils/resolveDragImage.js: -------------------------------------------------------------------------------- 1 | import { isString } from '@untemps/utils/string/isString' 2 | import { isElement } from '@untemps/utils/dom/isElement' 3 | 4 | export const resolveDragImage = (source) => { 5 | if (!!source) { 6 | if (isElement(source)) { 7 | return source 8 | } else if (source.src || isString(source)) { 9 | const image = new Image() 10 | image.src = source.src || source 11 | source.width && (image.width = source.width) 12 | source.height && (image.height = source.height) 13 | return image 14 | } 15 | } 16 | return null 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/index.yml: -------------------------------------------------------------------------------- 1 | name: "deploy" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - beta 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '16' 15 | - run: yarn install 16 | - run: yarn test:ci 17 | - run: yarn build 18 | - run: npx semantic-release 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 21 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | - uses: codecov/codecov-action@v1 23 | with: 24 | file: coverage/lcov.info -------------------------------------------------------------------------------- /dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear" 9 | }, 10 | "devDependencies": { 11 | "@rollup/plugin-commonjs": "^17.0.0", 12 | "@rollup/plugin-node-resolve": "^11.0.0", 13 | "rollup": "^2.3.4", 14 | "rollup-plugin-css-only": "^3.1.0", 15 | "rollup-plugin-livereload": "^2.0.0", 16 | "rollup-plugin-svelte": "^7.0.0", 17 | "rollup-plugin-terser": "^7.0.0", 18 | "svelte": "^3.0.0" 19 | }, 20 | "dependencies": { 21 | "sirv-cli": "^1.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /dev/src/CloseIcon.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "pluginSearchDirs": ["."], 3 | "printWidth": 120, 4 | "useTabs": true, 5 | "tabWidth": 4, 6 | "singleQuote": true, 7 | "semi": false, 8 | "trailingComma": "es5", 9 | "bracketSpacing": true, 10 | "overrides": [ 11 | { 12 | "files": "./**/*.js", 13 | "options": { 14 | "parser": "babel" 15 | } 16 | }, 17 | { 18 | "files": "./**/*.svelte", 19 | "options": { 20 | "svelteBracketNewLine": false, 21 | "svelteAllowShorthand": false, 22 | "svelteSortOrder" : "options-scripts-styles-markup", 23 | "plugins": [ 24 | "prettier-plugin-svelte" 25 | ] 26 | } 27 | }, 28 | { 29 | "files": "./**/*.json", 30 | "options": { 31 | "parser": "json" 32 | } 33 | }, 34 | { 35 | "files": ".prettierrc", 36 | "options": { 37 | "parser": "json" 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/__tests__/resolveDragImage.test.js: -------------------------------------------------------------------------------- 1 | import { createElement } from '@untemps/utils/dom/createElement' 2 | 3 | import { resolveDragImage } from '../resolveDragImage' 4 | 5 | describe('resolveDragImage', () => { 6 | let img = createElement() 7 | 8 | it.each([null, undefined, 0, {}])('returns null', (source) => { 9 | expect(resolveDragImage(source)).toBeNull() 10 | }) 11 | 12 | it('returns same element', () => { 13 | expect(resolveDragImage(img).isSameNode(img)).toBeTruthy() 14 | }) 15 | 16 | it.each([ 17 | [{ src: 'foo' }, createElement({ tag: 'img', attributes: { src: 'foo' } })], 18 | [ 19 | { src: 'foo', width: 100, height: 100 }, 20 | createElement({ tag: 'img', attributes: { src: 'foo', width: 100, height: 100 } }), 21 | ], 22 | ['foo', createElement({ tag: 'img', attributes: { src: 'foo' } })], 23 | ])('returns proper element', (source, expected) => { 24 | expect(resolveDragImage(source).isEqualNode(expected)).toBeTruthy() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "58 6 * * 5" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte' 2 | import babel from '@rollup/plugin-babel' 3 | import commonjs from '@rollup/plugin-commonjs' 4 | import postcss from 'rollup-plugin-postcss' 5 | import resolve from '@rollup/plugin-node-resolve' 6 | import { terser } from 'rollup-plugin-terser' 7 | import visualizer from 'rollup-plugin-visualizer' 8 | 9 | const production = process.env.NODE_ENV === 'production' 10 | const target = process.env.BABEL_ENV 11 | 12 | export default { 13 | input: 'src/index.js', 14 | output: { 15 | name: 'svelte-use-drop-outside', 16 | file: { 17 | cjs: 'dist/index.js', 18 | es: 'dist/index.es.js', 19 | umd: 'dist/index.umd.js', 20 | }[target], 21 | format: target, 22 | sourcemap: 'inline', 23 | }, 24 | plugins: [ 25 | svelte(), 26 | postcss({ 27 | plugins: [], 28 | }), 29 | babel({ 30 | exclude: 'node_modules/**', 31 | babelHelpers: 'bundled', 32 | }), 33 | resolve(), 34 | commonjs(), 35 | production && terser(), 36 | visualizer({ 37 | sourcemap: true, 38 | }), 39 | ], 40 | } 41 | -------------------------------------------------------------------------------- /src/useDropOutside.js: -------------------------------------------------------------------------------- 1 | import DragAndDrop from './DragAndDrop' 2 | 3 | const useDropOutside = ( 4 | node, 5 | { 6 | areaSelector, 7 | dragImage, 8 | dragClassName, 9 | animate, 10 | animateOptions, 11 | dragHandleCentered, 12 | onDropOutside, 13 | onDropInside, 14 | onDragCancel, 15 | } 16 | ) => { 17 | const instance = new DragAndDrop( 18 | node, 19 | areaSelector, 20 | dragImage, 21 | dragClassName, 22 | animate, 23 | animateOptions, 24 | dragHandleCentered, 25 | onDropOutside, 26 | onDropInside, 27 | onDragCancel 28 | ) 29 | 30 | return { 31 | update: ({ 32 | areaSelector, 33 | dragImage, 34 | dragClassName, 35 | animate, 36 | animateOptions, 37 | dragHandleCentered, 38 | onDropOutside, 39 | onDropInside, 40 | onDragCancel, 41 | }) => 42 | instance.update( 43 | areaSelector, 44 | dragImage, 45 | dragClassName, 46 | animate, 47 | animateOptions, 48 | dragHandleCentered, 49 | onDropOutside, 50 | onDropInside, 51 | onDragCancel 52 | ), 53 | destroy: () => instance.destroy(), 54 | } 55 | } 56 | 57 | export default useDropOutside 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vincent Le Badezet 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 | -------------------------------------------------------------------------------- /dev/public/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | position: relative; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | body { 12 | color: #333; 13 | margin: 0; 14 | padding: 0; 15 | box-sizing: border-box; 16 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 17 | } 18 | 19 | a { 20 | color: rgb(0,100,200); 21 | text-decoration: none; 22 | } 23 | 24 | a:hover { 25 | text-decoration: underline; 26 | } 27 | 28 | a:visited { 29 | color: rgb(0,80,160); 30 | } 31 | 32 | label { 33 | display: block; 34 | } 35 | 36 | input, button, select, textarea { 37 | font-family: inherit; 38 | font-size: inherit; 39 | -webkit-padding: 0.4em 0; 40 | padding: 0.4em; 41 | margin: 0 0 0.5em 0; 42 | box-sizing: border-box; 43 | border: 1px solid #ccc; 44 | border-radius: 2px; 45 | } 46 | 47 | input:disabled { 48 | color: #ccc; 49 | } 50 | 51 | button { 52 | color: #333; 53 | background-color: #f4f4f4; 54 | outline: none; 55 | } 56 | 57 | button:disabled { 58 | color: #999; 59 | } 60 | 61 | button:not(:disabled):active { 62 | background-color: #ddd; 63 | } 64 | 65 | button:focus { 66 | border-color: #666; 67 | } 68 | -------------------------------------------------------------------------------- /jest/jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | import { fireEvent } from '@testing-library/svelte' 3 | import { toBeInTheDocument, toHaveAttribute, toHaveStyle } from '@testing-library/jest-dom/matchers' 4 | import { standby } from '@untemps/utils/async/standby' 5 | 6 | expect.extend({ toBeInTheDocument, toHaveAttribute, toHaveStyle }) 7 | 8 | global._enter = async (trigger) => 9 | new Promise(async (resolve) => { 10 | await fireEvent.mouseOver(trigger) // fireEvent.mouseEnter only works if mouseOver is triggered before 11 | await fireEvent.mouseEnter(trigger) 12 | await standby(1) 13 | resolve() 14 | }) 15 | 16 | global._leave = async (trigger) => 17 | new Promise(async (resolve) => { 18 | await fireEvent.mouseLeave(trigger) 19 | await standby(1) 20 | resolve() 21 | }) 22 | 23 | global._enterAndLeave = async (trigger) => 24 | new Promise(async (resolve) => { 25 | await _enter(trigger) 26 | await _leave(trigger) 27 | resolve() 28 | }) 29 | 30 | global._focus = async (trigger) => 31 | new Promise(async (resolve) => { 32 | await fireEvent.focusIn(trigger) 33 | await standby(1) 34 | resolve() 35 | }) 36 | 37 | global._blur = async (trigger) => 38 | new Promise(async (resolve) => { 39 | await fireEvent.focusOut(trigger) 40 | await standby(1) 41 | resolve() 42 | }) 43 | 44 | global._focusAndBlur = async (trigger) => 45 | new Promise(async (resolve) => { 46 | await _focus(trigger) 47 | await _blur(trigger) 48 | resolve() 49 | }) 50 | 51 | global._keyDown = async (trigger, key) => 52 | new Promise(async (resolve) => { 53 | await fireEvent.keyDown(trigger, key || { key: 'Escape', code: 'Escape', charCode: 27 }) 54 | await standby(1) 55 | resolve() 56 | }) 57 | -------------------------------------------------------------------------------- /dev/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | import livereload from 'rollup-plugin-livereload' 5 | import { terser } from 'rollup-plugin-terser' 6 | import css from 'rollup-plugin-css-only' 7 | 8 | const production = !process.env.ROLLUP_WATCH 9 | 10 | function serve() { 11 | let server 12 | 13 | function toExit() { 14 | if (server) server.kill(0) 15 | } 16 | 17 | return { 18 | writeBundle() { 19 | if (server) return 20 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 21 | stdio: ['ignore', 'inherit', 'inherit'], 22 | shell: true, 23 | }) 24 | 25 | process.on('SIGTERM', toExit) 26 | process.on('exit', toExit) 27 | }, 28 | } 29 | } 30 | 31 | export default { 32 | input: 'src/main.js', 33 | output: { 34 | sourcemap: true, 35 | format: 'iife', 36 | name: 'app', 37 | file: 'public/build/bundle.js', 38 | }, 39 | plugins: [ 40 | svelte({ 41 | compilerOptions: { 42 | // enable run-time checks when not in production 43 | dev: !production, 44 | }, 45 | }), 46 | // we'll extract any component CSS out into 47 | // a separate file - better for performance 48 | css({ output: 'bundle.css' }), 49 | // If you have external dependencies installed from 50 | // npm, you'll most likely need these plugins. In 51 | // some cases you'll need additional configuration - 52 | // consult the documentation for details: 53 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 54 | resolve({ 55 | browser: true, 56 | dedupe: ['svelte'], 57 | }), 58 | commonjs(), 59 | 60 | // In dev mode, call `npm run start` once 61 | // the bundle has been generated 62 | !production && serve(), 63 | 64 | // Watch the `public` directory and refresh the 65 | // browser on changes when not in production 66 | !production && livereload('public'), 67 | 68 | // If we're building for production (npm run build 69 | // instead of npm run dev), minify 70 | production && terser(), 71 | ], 72 | watch: { 73 | clearScreen: false, 74 | }, 75 | } 76 | -------------------------------------------------------------------------------- /dev/src/SettingsIcon.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.6.0](https://github.com/untemps/svelte-use-drop-outside/compare/v1.5.0...v1.6.0) (2022-09-12) 2 | 3 | 4 | ### Features 5 | 6 | * Add dragHandleCentered prop ([#20](https://github.com/untemps/svelte-use-drop-outside/issues/20)) ([7946033](https://github.com/untemps/svelte-use-drop-outside/commit/7946033933f27e971b010bf2314a5a18d694cbdc)), closes [#19](https://github.com/untemps/svelte-use-drop-outside/issues/19) [#19](https://github.com/untemps/svelte-use-drop-outside/issues/19) [#19](https://github.com/untemps/svelte-use-drop-outside/issues/19) [#19](https://github.com/untemps/svelte-use-drop-outside/issues/19) [#19](https://github.com/untemps/svelte-use-drop-outside/issues/19) [#19](https://github.com/untemps/svelte-use-drop-outside/issues/19) [#19](https://github.com/untemps/svelte-use-drop-outside/issues/19) [#19](https://github.com/untemps/svelte-use-drop-outside/issues/19) [#19](https://github.com/untemps/svelte-use-drop-outside/issues/19) 7 | 8 | # [1.5.0](https://github.com/untemps/svelte-use-drop-outside/compare/v1.4.0...v1.5.0) (2022-09-11) 9 | 10 | 11 | ### Features 12 | 13 | * Implement action update ([#18](https://github.com/untemps/svelte-use-drop-outside/issues/18)) ([4c88abc](https://github.com/untemps/svelte-use-drop-outside/commit/4c88abce71316502af9411f91beb0c05d8d5b35c)) 14 | 15 | # [1.4.0](https://github.com/untemps/svelte-use-drop-outside/compare/v1.3.0...v1.4.0) (2022-09-06) 16 | 17 | 18 | ### Features 19 | 20 | * Animate drag cancellation and inside drop back ([#16](https://github.com/untemps/svelte-use-drop-outside/issues/16)) ([1195df8](https://github.com/untemps/svelte-use-drop-outside/commit/1195df83afddf68eef0cdd0261ee9db029471ac1)) 21 | 22 | # [1.3.0](https://github.com/untemps/svelte-use-drop-outside/compare/v1.2.0...v1.3.0) (2022-08-23) 23 | 24 | 25 | ### Features 26 | 27 | * Add new dragClassName prop ([#12](https://github.com/untemps/svelte-use-drop-outside/issues/12)) ([3744468](https://github.com/untemps/svelte-use-drop-outside/commit/3744468d0a85e3018dafd87e53b6f3a33bc30db0)) 28 | 29 | # [1.2.0](https://github.com/untemps/svelte-use-drop-outside/compare/v1.1.0...v1.2.0) (2022-07-31) 30 | 31 | 32 | ### Features 33 | 34 | * Add area to callbacks arguments ([#11](https://github.com/untemps/svelte-use-drop-outside/issues/11)) ([ff77cdc](https://github.com/untemps/svelte-use-drop-outside/commit/ff77cdc1fa03f6d54e9ab339b77ed233371f299e)) 35 | 36 | # [1.1.0](https://github.com/untemps/svelte-use-drop-outside/compare/v1.0.1...v1.1.0) (2022-07-26) 37 | 38 | 39 | ### Features 40 | 41 | * Add callback for drag cancellation ([#5](https://github.com/untemps/svelte-use-drop-outside/issues/5)) ([1c8d62c](https://github.com/untemps/svelte-use-drop-outside/commit/1c8d62c8836d1333d9e9597785d6dcff7fadad2d)) 42 | 43 | ## [1.0.1](https://github.com/untemps/svelte-use-drop-outside/compare/v1.0.0...v1.0.1) (2022-07-20) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * Trigger onDropOutside callback when target is dropped outside window boundaries ([#3](https://github.com/untemps/svelte-use-drop-outside/issues/3)) ([19497c5](https://github.com/untemps/svelte-use-drop-outside/commit/19497c56b0f1a9dc4d1833528c00404b1b7b1724)) 49 | 50 | # 1.0.0 (2022-07-18) 51 | 52 | 53 | ### Features 54 | 55 | * Initial commit ([42e2b9d](https://github.com/untemps/svelte-use-drop-outside/commit/42e2b9dbdef675e79c69a19738b86be6c6e114ef)) 56 | -------------------------------------------------------------------------------- /dev/README.md: -------------------------------------------------------------------------------- 1 | *Psst — looking for a more complete solution? Check out [SvelteKit](https://kit.svelte.dev), the official framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.* 2 | 3 | *Looking for a shareable component template instead? You can [use SvelteKit for that as well](https://kit.svelte.dev/docs#packaging) or the older [sveltejs/component-template](https://github.com/sveltejs/component-template)* 4 | 5 | --- 6 | 7 | # svelte app 8 | 9 | This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template. 10 | 11 | To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): 12 | 13 | ```bash 14 | npx degit sveltejs/template svelte-app 15 | cd svelte-app 16 | ``` 17 | 18 | *Note that you will need to have [Node.js](https://nodejs.org) installed.* 19 | 20 | 21 | ## Get started 22 | 23 | Install the dependencies... 24 | 25 | ```bash 26 | cd svelte-app 27 | npm install 28 | ``` 29 | 30 | ...then start [Rollup](https://rollupjs.org): 31 | 32 | ```bash 33 | npm run dev 34 | ``` 35 | 36 | Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. 37 | 38 | By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`. 39 | 40 | If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense. 41 | 42 | ## Building and running in production mode 43 | 44 | To create an optimised version of the app: 45 | 46 | ```bash 47 | npm run build 48 | ``` 49 | 50 | You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com). 51 | 52 | 53 | ## Single-page app mode 54 | 55 | By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere. 56 | 57 | If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json: 58 | 59 | ```js 60 | "start": "sirv public --single" 61 | ``` 62 | 63 | ## Using TypeScript 64 | 65 | This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with: 66 | 67 | ```bash 68 | node scripts/setupTypeScript.js 69 | ``` 70 | 71 | Or remove the script via: 72 | 73 | ```bash 74 | rm scripts/setupTypeScript.js 75 | ``` 76 | 77 | If you want to use `baseUrl` or `path` aliases within your `tsconfig`, you need to set up `@rollup/plugin-alias` to tell Rollup to resolve the aliases. For more info, see [this StackOverflow question](https://stackoverflow.com/questions/63427935/setup-tsconfig-path-in-svelte). 78 | 79 | ## Deploying to the web 80 | 81 | ### With [Vercel](https://vercel.com) 82 | 83 | Install `vercel` if you haven't already: 84 | 85 | ```bash 86 | npm install -g vercel 87 | ``` 88 | 89 | Then, from within your project folder: 90 | 91 | ```bash 92 | cd public 93 | vercel deploy --name my-project 94 | ``` 95 | 96 | ### With [surge](https://surge.sh/) 97 | 98 | Install `surge` if you haven't already: 99 | 100 | ```bash 101 | npm install -g surge 102 | ``` 103 | 104 | Then, from within your project folder: 105 | 106 | ```bash 107 | npm run build 108 | surge public my-project.surge.sh 109 | ``` 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@untemps/svelte-use-drop-outside", 3 | "version": "1.6.0", 4 | "author": "Vincent Le Badezet ", 5 | "license": "MIT", 6 | "description": "Svelte action to drop an element outside an area", 7 | "keywords": [ 8 | "drag", 9 | "drop", 10 | "dragndrop", 11 | "svelte", 12 | "svelte-action", 13 | "action", 14 | "javascript" 15 | ], 16 | "private": false, 17 | "repository": "https://github.com/untemps/svelte-use-drop-outside.git", 18 | "bugs": "https://github.com/untemps/svelte-use-drop-outside/issues", 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "main": "dist/index.js", 23 | "module": "dist/index.es.js", 24 | "svelte": "dist/index.es.js", 25 | "files": [ 26 | "dist" 27 | ], 28 | "devDependencies": { 29 | "@babel/cli": "^7.18.6", 30 | "@babel/core": "^7.18.6", 31 | "@babel/plugin-proposal-class-properties": "^7.18.6", 32 | "@babel/plugin-transform-runtime": "^7.18.6", 33 | "@babel/preset-env": "^7.18.6", 34 | "@commitlint/cli": "^17.0.3", 35 | "@commitlint/config-conventional": "^17.0.3", 36 | "@rollup/plugin-babel": "^5.3.1", 37 | "@rollup/plugin-commonjs": "^22.0.1", 38 | "@rollup/plugin-node-resolve": "^13.3.0", 39 | "@semantic-release/changelog": "^6.0.1", 40 | "@semantic-release/git": "^10.0.1", 41 | "@semantic-release/github": "^8.0.5", 42 | "@testing-library/dom": "^8.16.0", 43 | "@testing-library/jest-dom": "^5.16.4", 44 | "@testing-library/svelte": "^3.1.3", 45 | "babel-jest": "^28.1.3", 46 | "cross-env": "^7.0.3", 47 | "husky": "^8.0.1", 48 | "identity-obj-proxy": "^3.0.0", 49 | "jest": "^28.1.3", 50 | "jest-environment-jsdom": "^28.1.3", 51 | "postcss": "^8.4.16", 52 | "prettier": "^2.7.1", 53 | "prettier-plugin-svelte": "^2.7.0", 54 | "rimraf": "^3.0.2", 55 | "rollup": "^2.77.0", 56 | "rollup-plugin-postcss": "^4.0.2", 57 | "rollup-plugin-svelte": "^7.0.0", 58 | "rollup-plugin-terser": "^7.0.2", 59 | "rollup-plugin-visualizer": "^5.7.1", 60 | "semantic-release": "^19.0.3", 61 | "svelte-jester": "^2.3.2" 62 | }, 63 | "dependencies": { 64 | "@untemps/dom-observer": "^2.0.0", 65 | "@untemps/utils": "^2.2.0", 66 | "svelte": "3.49.0" 67 | }, 68 | "jest": { 69 | "testEnvironment": "jsdom", 70 | "transform": { 71 | "^.+\\.js$": "babel-jest", 72 | "^.+\\.svelte$": "svelte-jester" 73 | }, 74 | "moduleNameMapper": { 75 | "\\.(css|less|scss)$": "identity-obj-proxy" 76 | }, 77 | "moduleFileExtensions": [ 78 | "js", 79 | "svelte" 80 | ], 81 | "setupFilesAfterEnv": [ 82 | "/jest/jest.setup.js" 83 | ] 84 | }, 85 | "release": { 86 | "branches": [ 87 | "main", 88 | { 89 | "name": "beta", 90 | "prerelease": true 91 | } 92 | ], 93 | "plugins": [ 94 | [ 95 | "@semantic-release/commit-analyzer", 96 | { 97 | "releaseRules": [ 98 | { 99 | "type": "chore", 100 | "scope": "force", 101 | "release": "patch" 102 | } 103 | ] 104 | } 105 | ], 106 | "@semantic-release/release-notes-generator", 107 | "@semantic-release/changelog", 108 | "@semantic-release/npm", 109 | "@semantic-release/git", 110 | [ 111 | "@semantic-release/github", 112 | { 113 | "assets": [ 114 | { 115 | "path": "dist/index.js", 116 | "label": "CJS distribution" 117 | }, 118 | { 119 | "path": "dist/index.es.js", 120 | "label": "ES distribution" 121 | }, 122 | { 123 | "path": "dist/index.umd.js", 124 | "label": "UMD distribution" 125 | } 126 | ] 127 | } 128 | ] 129 | ] 130 | }, 131 | "scripts": { 132 | "dev": "cd dev && rimraf dist && yarn && yarn dev", 133 | "test": "jest -u --watch --coverage", 134 | "test:ci": "jest -u -b --ci --coverage", 135 | "build": "rimraf dist && yarn build:cjs && yarn build:es && yarn build:umd", 136 | "build:cjs": "cross-env NODE_ENV=production BABEL_ENV=cjs rollup -c", 137 | "build:es": "cross-env NODE_ENV=production BABEL_ENV=es rollup -c", 138 | "build:umd": "cross-env NODE_ENV=production BABEL_ENV=umd rollup -c", 139 | "prettier": "prettier \"*/**/*.js\" --ignore-path ./.prettierignore --write && git add . && git status", 140 | "prepare": "husky install" 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /dev/src/App.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 | 156 | 157 |
158 | {#if showSettings} 159 |
160 | 163 |
164 |

Settings

165 |
166 | 167 | 168 |
169 |
170 | 171 | 172 |
173 |
174 | 175 | 176 |
177 |
178 | 179 | 180 |
181 |
182 |
183 | {/if} 184 | 185 |
186 | 189 |
190 |
    191 | {#each colors as color, index} 192 |
  • 213 | {/each} 214 |
215 |
216 |
217 |
218 | -------------------------------------------------------------------------------- /src/DragAndDrop.js: -------------------------------------------------------------------------------- 1 | import { DOMObserver } from '@untemps/dom-observer' 2 | import { doElementsOverlap } from '@untemps/utils/dom/doElementsOverlap' 3 | import { getCSSDeclaration } from '@untemps/utils/dom/getCSSDeclaration' 4 | 5 | import { resolveDragImage } from './utils/resolveDragImage' 6 | 7 | import './useDropOutside.css' 8 | 9 | class DragAndDrop { 10 | static instances = [] 11 | 12 | #target = null 13 | #dragImage = null 14 | #dragClassName = null 15 | #animate = false 16 | #animateOptions = null 17 | #dragHandleCentered = false 18 | #onDropOutside = null 19 | #onDropInside = null 20 | #onDragCancel = null 21 | 22 | #observer = null 23 | #area = null 24 | #drag = null 25 | #holdX = 0 26 | #holdY = 0 27 | #dragWidth = 0 28 | #dragHeight = 0 29 | 30 | #boundMouseOverHandler = null 31 | #boundMouseOutHandler = null 32 | #boundMouseDownHandler = null 33 | #boundMouseMoveHandler = null 34 | #boundMouseUpHandler = null 35 | 36 | static destroy() { 37 | DragAndDrop.instances.forEach((instance) => { 38 | instance.destroy() 39 | }) 40 | DragAndDrop.instances = [] 41 | } 42 | 43 | constructor( 44 | target, 45 | areaSelector, 46 | dragImage, 47 | dragClassName, 48 | animate, 49 | animateOptions, 50 | dragHandleCentered, 51 | onDropOutside, 52 | onDropInside, 53 | onDragCancel 54 | ) { 55 | this.#target = target 56 | this.#dragImage = dragImage 57 | this.#dragClassName = dragClassName 58 | this.#animate = animate || false 59 | this.#animateOptions = { duration: 0.2, timingFunction: 'ease', ...(animateOptions || {}) } 60 | this.#dragHandleCentered = dragHandleCentered || false 61 | this.#onDropOutside = onDropOutside 62 | this.#onDropInside = onDropInside 63 | this.#onDragCancel = onDragCancel 64 | 65 | this.#area = document.querySelector(areaSelector) 66 | 67 | this.#drag = this.#createDrag() 68 | 69 | this.#boundMouseOverHandler = this.#onMouseOver.bind(this) 70 | this.#boundMouseOutHandler = this.#onMouseOut.bind(this) 71 | this.#boundMouseDownHandler = this.#onMouseDown.bind(this) 72 | 73 | this.#target.addEventListener('mouseover', this.#boundMouseOverHandler, false) 74 | this.#target.addEventListener('mouseout', this.#boundMouseOutHandler, false) 75 | this.#target.addEventListener('mousedown', this.#boundMouseDownHandler, false) 76 | this.#target.addEventListener('touchstart', this.#boundMouseDownHandler, false) 77 | 78 | DragAndDrop.instances.push(this) 79 | } 80 | 81 | update( 82 | areaSelector, 83 | dragImage, 84 | dragClassName, 85 | animate, 86 | animateOptions, 87 | dragHandleCentered, 88 | onDropOutside, 89 | onDropInside, 90 | onDragCancel 91 | ) { 92 | this.#dragImage = dragImage || this.#dragImage 93 | this.#dragClassName = dragClassName || this.#dragClassName 94 | this.#animate = animate !== undefined ? animate : this.#animate 95 | this.#animateOptions = animateOptions || this.#animateOptions 96 | this.#dragHandleCentered = dragHandleCentered !== undefined ? dragHandleCentered : this.#dragHandleCentered 97 | this.#onDropOutside = onDropOutside || this.#onDropOutside 98 | this.#onDropInside = onDropInside || this.#onDropInside 99 | this.#onDragCancel = onDragCancel || this.#onDragCancel 100 | 101 | this.#area = !!areaSelector ? document.querySelector(areaSelector) : this.#area 102 | 103 | this.#drag = this.#createDrag() 104 | } 105 | 106 | destroy() { 107 | this.#target.removeEventListener('mouseover', this.#boundMouseOverHandler) 108 | this.#target.removeEventListener('mouseout', this.#boundMouseOutHandler) 109 | this.#target.removeEventListener('mousedown', this.#boundMouseDownHandler) 110 | this.#target.removeEventListener('touchstart', this.#boundMouseDownHandler) 111 | 112 | this.#boundMouseOverHandler = null 113 | this.#boundMouseOutHandler = null 114 | this.#boundMouseDownHandler = null 115 | 116 | this.#observer?.clear() 117 | this.#observer = null 118 | } 119 | 120 | #createDrag() { 121 | const drag = this.#dragImage ? resolveDragImage(this.#dragImage) : this.#target.cloneNode(true) 122 | drag.setAttribute('draggable', false) 123 | drag.setAttribute('id', 'drag') 124 | drag.setAttribute('role', 'presentation') 125 | drag.classList.add('__drag') 126 | if (!!this.#dragClassName) { 127 | const cssText = getCSSDeclaration(this.#dragClassName, true) 128 | if (!!cssText) { 129 | drag.style.cssText = cssText 130 | } 131 | } 132 | 133 | this.#observer = new DOMObserver() 134 | this.#observer.wait(drag, null, { events: [DOMObserver.ADD] }).then(() => { 135 | const { width, height } = drag.getBoundingClientRect() 136 | this.#dragWidth = width 137 | this.#dragHeight = height 138 | }) 139 | 140 | return drag 141 | } 142 | 143 | #animateBack(callback) { 144 | if (this.#animate) { 145 | const { width, height, left, top } = this.#target.getBoundingClientRect() 146 | this.#drag.style.setProperty( 147 | '--origin-x', 148 | left - (this.#dragHandleCentered ? (this.#dragWidth - width) >> 1 : 0) + 'px' 149 | ) 150 | this.#drag.style.setProperty( 151 | '--origin-y', 152 | top - (this.#dragHandleCentered ? (this.#dragHeight - height) >> 1 : 0) + 'px' 153 | ) 154 | this.#drag.style.animation = `move ${this.#animateOptions.duration}s ${this.#animateOptions.timingFunction}` 155 | this.#drag.addEventListener( 156 | 'animationend', 157 | () => { 158 | this.#drag.style.animation = 'none' 159 | this.#drag.remove() 160 | callback?.(this.#target, this.#area) 161 | }, 162 | false 163 | ) 164 | } else { 165 | this.#drag.remove() 166 | callback?.(this.#target, this.#area) 167 | } 168 | } 169 | 170 | #onMouseOver(e) { 171 | e.target.style.cursor = 'grab' 172 | } 173 | 174 | #onMouseOut(e) { 175 | e.target.style.cursor = 'default' 176 | } 177 | 178 | #onMouseMove(e) { 179 | if (this.#drag.style.visibility === 'hidden') { 180 | this.#drag.style.visibility = 'visible' 181 | } 182 | 183 | const pageX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX 184 | const pageY = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY 185 | 186 | this.#drag.style.left = pageX - (this.#dragHandleCentered ? this.#dragWidth >> 1 : this.#holdX) + 'px' 187 | this.#drag.style.top = pageY - (this.#dragHandleCentered ? this.#dragHeight >> 1 : this.#holdY) + 'px' 188 | } 189 | 190 | #onMouseDown(e) { 191 | const clientX = e.type === 'touchstart' ? e.targetTouches[0].clientX : e.clientX 192 | const clientY = e.type === 'touchstart' ? e.targetTouches[0].clientY : e.clientY 193 | this.#holdX = clientX - this.#target.getBoundingClientRect().left 194 | this.#holdY = clientY - this.#target.getBoundingClientRect().top 195 | 196 | this.#drag.style.visibility = 'hidden' 197 | this.#drag.style.cursor = 'grabbing' 198 | 199 | this.#boundMouseMoveHandler = this.#onMouseMove.bind(this) 200 | this.#boundMouseUpHandler = this.#onMouseUp.bind(this) 201 | 202 | document.addEventListener('mousemove', this.#boundMouseMoveHandler, false) 203 | document.addEventListener('mouseup', this.#boundMouseUpHandler, false) 204 | document.addEventListener('touchmove', this.#boundMouseMoveHandler, false) 205 | document.addEventListener('keydown', this.#boundMouseUpHandler) 206 | this.#target.addEventListener('touchend', this.#boundMouseUpHandler, false) 207 | this.#target.addEventListener('touchcancel', this.#boundMouseUpHandler, false) 208 | 209 | this.#target.parentNode.appendChild(this.#drag) 210 | } 211 | 212 | #onMouseUp(e) { 213 | if (e.type.startsWith('key') && e.key !== 'Escape') { 214 | return 215 | } 216 | 217 | document.removeEventListener('mousemove', this.#boundMouseMoveHandler) 218 | document.removeEventListener('mouseup', this.#boundMouseUpHandler) 219 | document.removeEventListener('touchmove', this.#boundMouseMoveHandler) 220 | document.removeEventListener('keydown', this.#boundMouseUpHandler) 221 | this.#target.removeEventListener('touchend', this.#boundMouseUpHandler) 222 | this.#target.removeEventListener('touchcancel', this.#boundMouseUpHandler) 223 | 224 | this.#boundMouseMoveHandler = null 225 | this.#boundMouseUpHandler = null 226 | 227 | const doOverlap = doElementsOverlap(this.#area, this.#drag) 228 | 229 | if (e.type.startsWith('key')) { 230 | this.#animateBack(this.#onDragCancel) 231 | } else if (doOverlap) { 232 | this.#animateBack(this.#onDropInside) 233 | } else { 234 | this.#drag.remove() 235 | this.#onDropOutside?.(this.#target, this.#area) 236 | } 237 | } 238 | } 239 | 240 | export default DragAndDrop 241 | -------------------------------------------------------------------------------- /src/__tests__/useDropOutside.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { fireEvent, screen } from '@testing-library/dom' 6 | 7 | import { createElement } from '@untemps/utils/dom/createElement' 8 | import { getElement } from '@untemps/utils/dom/getElement' 9 | 10 | import DragAndDrop from '../DragAndDrop' 11 | import useDropOutside from '../useDropOutside' 12 | 13 | const areaSize = 200 14 | const targetSize = 100 15 | 16 | const createStyle = () => { 17 | createElement({ 18 | tag: 'style', 19 | textContent: ` 20 | .gag { 21 | background-color: black; 22 | } 23 | .pol { 24 | background-color: red; 25 | } 26 | `, 27 | parent: document.body, 28 | }) 29 | } 30 | 31 | const createArea = (id) => { 32 | const el = createElement({ 33 | tag: 'div', 34 | attributes: { id, class: 'foo', style: `width: ${areaSize}; height: ${areaSize};` }, 35 | parent: document.body, 36 | }) 37 | el.getBoundingClientRect = () => ({ 38 | width: areaSize, 39 | height: areaSize, 40 | top: 0, 41 | left: 0, 42 | right: areaSize, 43 | bottom: areaSize, 44 | }) 45 | return el 46 | } 47 | 48 | const createTarget = (id, parent) => { 49 | return createElement({ 50 | tag: 'div', 51 | attributes: { id, class: 'bar', style: `width: ${targetSize}; height: ${targetSize};` }, 52 | parent, 53 | }) 54 | } 55 | 56 | describe('useDropOutside', () => { 57 | let area, 58 | target, 59 | options = null 60 | 61 | let action = null 62 | 63 | beforeEach(() => { 64 | createStyle() 65 | area = createArea('area') 66 | target = createTarget('target', area) 67 | options = { 68 | areaSelector: '#area', 69 | } 70 | }) 71 | 72 | afterEach(() => { 73 | area = null 74 | target = null 75 | options = null 76 | 77 | fireEvent.mouseUp(document) 78 | 79 | document.body.innerHTML = '' 80 | 81 | action?.destroy() 82 | 83 | DragAndDrop.destroy() 84 | }) 85 | 86 | describe('init', () => { 87 | it('Sets pointer to grab on mouseover', async () => { 88 | action = useDropOutside(target, options) 89 | 90 | fireEvent.mouseOver(target) 91 | 92 | expect(target.style.cursor).toBe('grab') 93 | }) 94 | 95 | it('Sets pointer to default on mouseout', async () => { 96 | action = useDropOutside(target, options) 97 | 98 | fireEvent.mouseOver(target) 99 | fireEvent.mouseOut(target) 100 | 101 | expect(target.style.cursor).toBe('default') 102 | }) 103 | 104 | it('Sets pointer to grabbing on mousedown', async () => { 105 | action = useDropOutside(target, options) 106 | 107 | fireEvent.mouseDown(target) 108 | fireEvent.mouseMove(document) 109 | 110 | const drag = getElement('#drag') 111 | 112 | expect(drag.style.cursor).toBe('grabbing') 113 | }) 114 | 115 | it('Removes dragged element on mouseup', async () => { 116 | action = useDropOutside(target, options) 117 | 118 | fireEvent.mouseDown(target) 119 | fireEvent.mouseMove(document) 120 | 121 | const drag = getElement('#drag') 122 | 123 | expect(drag).toBeInTheDocument() 124 | 125 | fireEvent.mouseUp(document) 126 | 127 | expect(drag).not.toBeInTheDocument() 128 | }) 129 | 130 | it('Triggers onDropInside callback', async () => { 131 | const onDropInside = jest.fn() 132 | action = useDropOutside(target, { ...options, animate: true, onDropInside }) 133 | 134 | fireEvent.touchStart(target, { targetTouches: [{ pageX: 10, pageY: 10 }] }) 135 | fireEvent.touchMove(document, { targetTouches: [{ pageX: 10, pageY: 10 }] }) 136 | fireEvent.touchMove(document, { targetTouches: [{ pageX: 10, pageY: 10 }] }) // Duplicate on purpose 137 | 138 | const drag = document.querySelector('#drag') 139 | drag.getBoundingClientRect = () => ({ 140 | width: targetSize, 141 | height: targetSize, 142 | top: 0, 143 | left: 0, 144 | right: targetSize, 145 | bottom: targetSize, 146 | }) 147 | 148 | fireEvent.mouseUp(document) 149 | fireEvent.animationEnd(screen.getByRole('presentation')) 150 | 151 | expect(onDropInside).toHaveBeenCalled() 152 | }) 153 | 154 | it('Triggers onDropInside callback on update', async () => { 155 | const onDropInside = jest.fn() 156 | const onDropInsideRepl = jest.fn() 157 | action = useDropOutside(target, { ...options, onDropInside }) 158 | action.update({ onDropInside: onDropInsideRepl }) 159 | 160 | fireEvent.touchStart(target, { targetTouches: [{ pageX: 10, pageY: 10 }] }) 161 | fireEvent.touchMove(document, { targetTouches: [{ pageX: 10, pageY: 10 }] }) 162 | fireEvent.touchMove(document, { targetTouches: [{ pageX: 10, pageY: 10 }] }) // Duplicate on purpose 163 | 164 | const drag = document.querySelector('#drag') 165 | drag.getBoundingClientRect = () => ({ 166 | width: targetSize, 167 | height: targetSize, 168 | top: 0, 169 | left: 0, 170 | right: targetSize, 171 | bottom: targetSize, 172 | }) 173 | 174 | fireEvent.mouseUp(document) 175 | 176 | expect(onDropInsideRepl).toHaveBeenCalled() 177 | }) 178 | 179 | it('Triggers onDropOutside callback', async () => { 180 | const onDropOutside = jest.fn() 181 | action = useDropOutside(target, { ...options, onDropOutside }) 182 | 183 | fireEvent.mouseDown(target) 184 | fireEvent.mouseMove(document) 185 | 186 | const drag = document.querySelector('#drag') 187 | drag.getBoundingClientRect = () => ({ 188 | width: targetSize, 189 | height: targetSize, 190 | top: areaSize + 10, 191 | left: areaSize + 10, 192 | right: areaSize + 10 + targetSize, 193 | bottom: areaSize + 10 + targetSize, 194 | }) 195 | 196 | fireEvent.mouseUp(document) 197 | 198 | expect(onDropOutside).toHaveBeenCalled() 199 | }) 200 | 201 | it('Triggers onDropOutside callback on update', async () => { 202 | const onDropOutside = jest.fn() 203 | const onDropOutsideRepl = jest.fn() 204 | action = useDropOutside(target, { ...options, onDropOutside }) 205 | action.update({ onDropOutside: onDropOutsideRepl }) 206 | 207 | fireEvent.mouseDown(target) 208 | fireEvent.mouseMove(document) 209 | 210 | const drag = document.querySelector('#drag') 211 | drag.getBoundingClientRect = () => ({ 212 | width: targetSize, 213 | height: targetSize, 214 | top: areaSize + 10, 215 | left: areaSize + 10, 216 | right: areaSize + 10 + targetSize, 217 | bottom: areaSize + 10 + targetSize, 218 | }) 219 | 220 | fireEvent.mouseUp(document) 221 | 222 | expect(onDropOutsideRepl).toHaveBeenCalled() 223 | }) 224 | 225 | it('Triggers onDragCancel callback', async () => { 226 | const onDragCancel = jest.fn() 227 | action = useDropOutside(target, { ...options, animate: true, onDragCancel }) 228 | 229 | fireEvent.mouseDown(target) 230 | fireEvent.mouseMove(document) 231 | fireEvent.keyDown(document, { key: 'A', code: 'A' }) 232 | fireEvent.keyDown(document, { key: 'Escape', code: 'Esc' }) 233 | fireEvent.animationEnd(screen.getByRole('presentation')) 234 | 235 | expect(onDragCancel).toHaveBeenCalled() 236 | }) 237 | 238 | it('Triggers onDragCancel callback on update', async () => { 239 | const onDragCancel = jest.fn() 240 | const onDragCancelRepl = jest.fn() 241 | action = useDropOutside(target, { ...options, animate: true, onDragCancel }) 242 | action.update({ animate: false, onDragCancel: onDragCancelRepl }) 243 | 244 | fireEvent.mouseDown(target) 245 | fireEvent.mouseMove(document) 246 | fireEvent.keyDown(document, { key: 'A', code: 'A' }) 247 | fireEvent.keyDown(document, { key: 'Escape', code: 'Esc' }) 248 | 249 | expect(onDragCancelRepl).toHaveBeenCalled() 250 | }) 251 | 252 | it('Sets custom class to dragged element', async () => { 253 | action = useDropOutside(target, { ...options, dragClassName: 'gag' }) 254 | 255 | fireEvent.mouseDown(target) 256 | fireEvent.mouseMove(document) 257 | 258 | expect(screen.getByRole('presentation')).toBeInTheDocument() 259 | expect(screen.getByRole('presentation')).toHaveStyle('background-color: black;') 260 | }) 261 | 262 | it('Sets custom class to dragged element on update', async () => { 263 | action = useDropOutside(target, { ...options, dragClassName: 'gag' }) 264 | action.update({ dragClassName: 'pol' }) 265 | 266 | fireEvent.mouseDown(target) 267 | fireEvent.mouseMove(document) 268 | 269 | expect(screen.getByRole('presentation')).toBeInTheDocument() 270 | expect(screen.getByRole('presentation')).toHaveStyle('background-color: red;') 271 | }) 272 | 273 | it('Sets unknown custom class to dragged element', async () => { 274 | action = useDropOutside(target, { ...options, dragClassName: 'sur' }) 275 | 276 | fireEvent.mouseDown(target) 277 | fireEvent.mouseMove(document) 278 | 279 | expect(screen.getByRole('presentation')).toBeInTheDocument() 280 | expect(screen.getByRole('presentation')).not.toHaveStyle('background-color: black;') 281 | expect(screen.getByRole('presentation')).not.toHaveStyle('background-color: red;') 282 | }) 283 | 284 | it('Sets drag image', async () => { 285 | const dragImage = createElement({ tag: 'img', attributes: { src: 'foo', alt: 'bar' } }) 286 | action = useDropOutside(target, { ...options, dragImage, dragHandleCentered: true }) 287 | 288 | fireEvent.mouseDown(target) 289 | fireEvent.mouseMove(document) 290 | 291 | expect(screen.getByAltText('bar')).toBeInTheDocument() 292 | }) 293 | 294 | it('Sets drag image on update', async () => { 295 | const dragImage = createElement({ tag: 'img', attributes: { src: 'foo', alt: 'bar' } }) 296 | const dragImageRepl = createElement({ tag: 'img', attributes: { src: 'bar', alt: 'foo' } }) 297 | action = useDropOutside(target, { ...options, dragImage }) 298 | action.update({ dragImage: dragImageRepl, dragHandleCentered: false }) 299 | 300 | fireEvent.mouseDown(target) 301 | fireEvent.mouseMove(document) 302 | 303 | expect(screen.getByAltText('foo')).toBeInTheDocument() 304 | }) 305 | 306 | it('Stores and clears instances in static class property', async () => { 307 | useDropOutside(target, options) 308 | useDropOutside(target, options) 309 | useDropOutside(target, options) 310 | 311 | expect(DragAndDrop.instances).toHaveLength(3) 312 | 313 | DragAndDrop.destroy() 314 | 315 | expect(DragAndDrop.instances).toHaveLength(0) 316 | }) 317 | }) 318 | }) 319 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Drop an element outside an area 3 |

4 |

5 | svelte-use-drop-outside 6 |

7 |

8 | Svelte action to drop an element outside an area and more... 9 |

10 | 11 | [![npm](https://img.shields.io/npm/v/@untemps/svelte-use-drop-outside?style=for-the-badge)](https://www.npmjs.com/package/@untemps/svelte-use-drop-outside) 12 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/untemps/svelte-use-drop-outside/index.yml?style=for-the-badge)](https://github.com/untemps/svelte-use-drop-outside/actions) 13 | [![Codecov](https://img.shields.io/codecov/c/github/untemps/svelte-use-drop-outside?style=for-the-badge)](https://codecov.io/gh/untemps/svelte-use-drop-outside) 14 | 15 | ## Demo 16 | 17 |

18 | :red_circle: LIVE DEMO :red_circle: 19 |

20 | 21 | ## Installation 22 | 23 | ```bash 24 | yarn add @untemps/svelte-use-drop-outside 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Basic usage 30 | 31 | ```svelte 32 | 39 | 40 |
41 |
42 |
43 |
51 | Drag me outside the white area 52 |
53 |
54 |
55 |
56 | 57 | 96 | ``` 97 | 98 | ## API 99 | 100 | | Props | Type | Default | Description | 101 | |----------------------|-----------------------------|------------------------------------------|----------------------------------------------------------------------------------------| 102 | | `areaSelector` | string | null | Selector of the element considered as the "inside" area. | 103 | | `dragImage` | element or object or string | null | The image used when the element is dragging. | 104 | | `dragClassName` | string | null | A class name that will be assigned to the dragged element. | 105 | | `animate` | boolean | false | A flag to enable animation back. | 106 | | `animateOptions` | object | { duration: .2, timingFunction: 'ease' } | Optional options for the animation back (see [Animation Options](#animation-options)). | 107 | | `dragHandleCentered` | boolean | false | A flag to handle the dragged element by its center. | 108 | | `onDropOutside` | function | null | Callback triggered when the dragged element is dropped outside the area. | 109 | | `onDropInside` | function | null | Callback triggered when the dragged element is dropped inside the area | 110 | | `onDragCancel` | function | null | Callback triggered when the drag is cancelled (Esc key) | 111 | 112 | ### Area Selector 113 | 114 | You can define the DOM element which will be treated as the "inside" area by passing the [selector](https://developer.mozilla.org/fr/docs/Web/API/Document/querySelector) of this element. 115 | 116 | When dropping the dragged element, the action reconciles the boundaries of this element with the boundaries of the area to assert inside/outside stuff. 117 | 118 | When pressing the `Escape` key, wherever the dragged element is, it is put back to its original position. 119 | 120 | ### Drag Image 121 | 122 | By default, the action clones the target element and sets its opacity to `0.7`. 123 | 124 | Alternately, you may use the `dragImage` prop to customize the image displayed during the drag. 125 | 126 | The `dragImage` prop may be: 127 | 128 | #### A DOM element such a `` or a `