Repository: taye/interact.js Branch: main Commit: d3d474610c11 Files: 287 Total size: 604.4 KB Directory structure: gitextract_w464gyf3/ ├── .browserslistrc ├── .codeclimate.yml ├── .eslintignore ├── .eslintrc.cjs ├── .github/ │ ├── stale.yml │ └── workflows/ │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .husky/ │ ├── .gitignore │ └── pre-commit ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .stylelintrc.cjs ├── .yarnrc ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── babel.config.cjs ├── bin/ │ ├── _add_plugin_indexes │ ├── _bundle │ ├── _check_deps │ ├── _clean │ ├── _link │ ├── _lint │ ├── _release │ ├── _types │ └── _version ├── bundle.rollup.config.cjs ├── docs/ │ ├── action-options.md │ ├── auto-start.md │ ├── draggable.md │ ├── dropzone.md │ ├── events.md │ ├── faq.md │ ├── gesturable.md │ ├── inertia.md │ ├── installation.md │ ├── introduction.md │ ├── migrating.md │ ├── modifiers.md │ ├── reflow.md │ ├── resizable.md │ ├── restriction.md │ ├── snapping.md │ └── tooling.md ├── esnext.rollup.config.cjs ├── examples/ │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── dropzones/ │ │ ├── index.css │ │ ├── index.html │ │ └── index.js │ ├── events/ │ │ ├── index.css │ │ ├── index.html │ │ └── index.js │ ├── iframes/ │ │ ├── bottom.html │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ └── middle.html │ ├── snap/ │ │ ├── index.css │ │ ├── index.html │ │ └── index.js │ ├── sortable/ │ │ ├── index.html │ │ ├── react.js │ │ ├── shared.js │ │ ├── style.css │ │ └── vue.js │ ├── star/ │ │ ├── index.css │ │ └── index.js │ ├── svg-editor/ │ │ ├── index.html │ │ └── index.js │ └── transform/ │ ├── index.html │ └── index.js ├── jest.config.ts ├── lerna.json ├── package.json ├── packages/ │ ├── .eslintrc.cjs │ ├── @interactjs/ │ │ ├── actions/ │ │ │ ├── README.md │ │ │ ├── actions.spec.ts │ │ │ ├── drag/ │ │ │ │ ├── drag.spec.ts │ │ │ │ └── plugin.ts │ │ │ ├── drop/ │ │ │ │ ├── DropEvent.spec.ts │ │ │ │ ├── DropEvent.ts │ │ │ │ ├── drop.spec.ts │ │ │ │ └── plugin.ts │ │ │ ├── gesture/ │ │ │ │ ├── gesture.spec.ts │ │ │ │ └── plugin.ts │ │ │ ├── package.json │ │ │ ├── plugin.ts │ │ │ └── resize/ │ │ │ ├── plugin.ts │ │ │ └── resize.spec.ts │ │ ├── auto-scroll/ │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ └── plugin.ts │ │ ├── auto-start/ │ │ │ ├── InteractableMethods.ts │ │ │ ├── README.md │ │ │ ├── autoStart.spec.ts │ │ │ ├── base.ts │ │ │ ├── dragAxis.ts │ │ │ ├── hold.spec.ts │ │ │ ├── hold.ts │ │ │ ├── package.json │ │ │ └── plugin.ts │ │ ├── core/ │ │ │ ├── BaseEvent.ts │ │ │ ├── Eventable.spec.ts │ │ │ ├── Eventable.ts │ │ │ ├── InteractEvent.ts │ │ │ ├── InteractStatic.ts │ │ │ ├── Interactable.spec.ts │ │ │ ├── Interactable.ts │ │ │ ├── InteractableSet.ts │ │ │ ├── Interaction.spec.ts │ │ │ ├── Interaction.ts │ │ │ ├── NativeTypes.ts │ │ │ ├── PointerInfo.ts │ │ │ ├── README.md │ │ │ ├── events.ts │ │ │ ├── interactablePreventDefault.spec.ts │ │ │ ├── interactablePreventDefault.ts │ │ │ ├── interactionFinder.spec.ts │ │ │ ├── interactionFinder.ts │ │ │ ├── interactions.spec.ts │ │ │ ├── interactions.ts │ │ │ ├── options.ts │ │ │ ├── package.json │ │ │ ├── scope.spec.ts │ │ │ ├── scope.ts │ │ │ ├── tests/ │ │ │ │ └── _helpers.ts │ │ │ └── types.ts │ │ ├── dev-tools/ │ │ │ ├── README.md │ │ │ ├── babel-plugin-prod.js │ │ │ ├── babel-plugin-prod.spec.ts │ │ │ ├── devTools.spec.ts │ │ │ ├── package.json │ │ │ ├── plugin.ts │ │ │ └── visualizer/ │ │ │ ├── plugin.stub.ts │ │ │ ├── plugin.ts │ │ │ ├── visualizer.spec.stub.ts │ │ │ ├── visualizer.spec.ts │ │ │ ├── vueModules.stub.ts │ │ │ └── vueModules.ts │ │ ├── inertia/ │ │ │ ├── README.md │ │ │ ├── inertia.spec.ts │ │ │ ├── package.json │ │ │ └── plugin.ts │ │ ├── interact/ │ │ │ ├── README.md │ │ │ ├── index.ts │ │ │ ├── interact.spec.ts │ │ │ └── package.json │ │ ├── interactjs/ │ │ │ ├── index.stub.ts │ │ │ ├── index.ts │ │ │ └── package.json │ │ ├── modifiers/ │ │ │ ├── Modification.ts │ │ │ ├── README.md │ │ │ ├── all.ts │ │ │ ├── aspectRatio.spec.ts │ │ │ ├── aspectRatio.ts │ │ │ ├── avoid/ │ │ │ │ ├── avoid.stub.ts │ │ │ │ └── avoid.ts │ │ │ ├── base.spec.ts │ │ │ ├── base.ts │ │ │ ├── noop.ts │ │ │ ├── package.json │ │ │ ├── plugin.ts │ │ │ ├── restrict/ │ │ │ │ ├── edges.spec.ts │ │ │ │ ├── edges.ts │ │ │ │ ├── pointer.spec.ts │ │ │ │ ├── pointer.ts │ │ │ │ ├── rect.ts │ │ │ │ ├── size.spec.ts │ │ │ │ └── size.ts │ │ │ ├── rubberband/ │ │ │ │ ├── rubberband.stub.ts │ │ │ │ └── rubberband.ts │ │ │ ├── snap/ │ │ │ │ ├── edges.spec.ts │ │ │ │ ├── edges.ts │ │ │ │ ├── pointer.spec.ts │ │ │ │ ├── pointer.ts │ │ │ │ ├── size.spec.ts │ │ │ │ └── size.ts │ │ │ ├── spring/ │ │ │ │ ├── spring.stub.ts │ │ │ │ └── spring.ts │ │ │ ├── transform/ │ │ │ │ ├── transform.stub.ts │ │ │ │ └── transform.ts │ │ │ └── types.ts │ │ ├── offset/ │ │ │ ├── offset.spec.ts │ │ │ ├── package.json │ │ │ └── plugin.ts │ │ ├── pointer-events/ │ │ │ ├── PointerEvent.spec.ts │ │ │ ├── PointerEvent.ts │ │ │ ├── README.md │ │ │ ├── base.spec.ts │ │ │ ├── base.ts │ │ │ ├── holdRepeat.spec.ts │ │ │ ├── holdRepeat.ts │ │ │ ├── interactableTargets.ts │ │ │ ├── package.json │ │ │ └── plugin.ts │ │ ├── reflow/ │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── plugin.ts │ │ │ └── reflow.spec.ts │ │ ├── snappers/ │ │ │ ├── all.ts │ │ │ ├── edgeTarget.stub.ts │ │ │ ├── edgeTarget.ts │ │ │ ├── elements.stub.ts │ │ │ ├── elements.ts │ │ │ ├── grid.ts │ │ │ ├── package.json │ │ │ └── plugin.ts │ │ ├── types/ │ │ │ ├── README.md │ │ │ ├── index.ts │ │ │ ├── package.json │ │ │ └── types.spec.ts │ │ └── utils/ │ │ ├── ElementState.stub.ts │ │ ├── ElementState.ts │ │ ├── README.md │ │ ├── arr.ts │ │ ├── browser.ts │ │ ├── center.ts │ │ ├── clone.ts │ │ ├── displace.stub.ts │ │ ├── displace.ts │ │ ├── domObjects.ts │ │ ├── domUtils.spec.ts │ │ ├── domUtils.ts │ │ ├── exchange.stub.ts │ │ ├── exchange.ts │ │ ├── extend.ts │ │ ├── getOriginXY.ts │ │ ├── hypot.ts │ │ ├── is.ts │ │ ├── isNonNativeEvent.ts │ │ ├── isWindow.ts │ │ ├── misc.ts │ │ ├── normalizeListeners.spec.ts │ │ ├── normalizeListeners.ts │ │ ├── package.json │ │ ├── pointerExtend.ts │ │ ├── pointerUtils.ts │ │ ├── raf.ts │ │ ├── rect.ts │ │ ├── shallowEqual.ts │ │ └── window.ts │ └── interactjs/ │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── bower.json │ ├── index.ts │ └── package.json ├── scripts/ │ ├── .eslintrc.cjs │ ├── addPluginIndexes.js │ ├── babel/ │ │ ├── absolute-imports.js │ │ ├── inline-env-vars.js │ │ ├── relative-imports.js │ │ └── vue-sfc.js │ ├── bin/ │ │ ├── _check_deps.js │ │ ├── add_plugin_indexes.js │ │ ├── bundle.js │ │ ├── clean.js │ │ ├── lint.js │ │ ├── release.js │ │ ├── types.js │ │ └── version.js │ ├── execTypes.js │ ├── getVersion.js │ ├── headers.js │ └── utils.js ├── shims.d.ts ├── test/ │ ├── .eslintrc.cjs │ └── fixtures/ │ ├── babelPluginProject/ │ │ ├── index.js │ │ └── node_modules/ │ │ └── @interactjs/ │ │ └── a/ │ │ ├── a.js │ │ ├── b/ │ │ │ ├── b.js │ │ │ └── index.js │ │ ├── package-main-file.js │ │ └── package.json │ └── dependentTsProject/ │ ├── index.ts │ └── tsconfig.json ├── tsconfig.json ├── typedoc.config.cjs ├── types.tsconfig.json ├── vijest.config.js └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .browserslistrc ================================================ defaults and fully supports es6-module node >= 16 ================================================ FILE: .codeclimate.yml ================================================ plugins: eslint: enabled: false channel: eslint-6 extensions: - .ts - .js duplication: enabled: false config: languages: - javascript - typescript ratings: paths: - "packages/**/*.ts" exclude_paths: - "**/examples/" - "**/build/" - "**/docs/" - "**/_*" - "**/*.spec.ts" - "**/*.d.ts" ================================================ FILE: .eslintignore ================================================ dist coverage **/node_modules/** _angular ================================================ FILE: .eslintrc.cjs ================================================ module.exports = { extends: [ 'plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript', 'plugin:react/all', 'standard', 'prettier', ], settings: { 'import/resolver': { typescript: null }, react: { version: '16' }, }, env: { commonjs: true, es6: true, node: true, }, parser: '@typescript-eslint/parser', parserOptions: { sourceType: 'module', ecmaVersion: 2020, }, plugins: ['@typescript-eslint', 'eslint-plugin-tsdoc', 'markdown'], globals: { globalThis: false, }, rules: { 'linebreak-style': ['error', 'unix'], 'lines-between-class-members': 'off', 'no-caller': 'error', 'no-console': 'off', 'no-empty': 'off', 'no-prototype-builtins': 'off', 'no-shadow': 'error', 'no-useless-constructor': 'off', 'no-var': 'error', 'import/no-extraneous-dependencies': ['error', { devDependencies: false }], 'import/order': [ 'error', { alphabetize: { order: 'asc', caseInsensitive: true }, 'newlines-between': 'always', groups: ['builtin', 'external', 'internal', 'parent', 'index', 'sibling'], pathGroups: [{ pattern: '@interactjs/**', group: 'internal' }], }, ], 'operator-linebreak': 'off', 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], 'prefer-const': 'error', 'standard/array-bracket-even-spacing': 'off', 'standard/computed-property-even-spacing': 'off', 'standard/object-curly-even-spacing': 'off', 'tsdoc/syntax': 'warn', '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], '@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/explicit-member-accessibility': 'off', '@typescript-eslint/member-accessibility': 'off', '@typescript-eslint/no-empty-interface': 'error', '@typescript-eslint/no-inferrable-types': 'error', '@typescript-eslint/no-use-before-define': 'off', }, overrides: [ { files: '*.{ts{,x},vue}', rules: { 'import/named': 'off', 'import/no-named-as-default': 'off', 'import/no-unresolved': 'off', 'no-redeclare': 'off', 'no-shadow': 'off', 'no-undef': 'off', 'no-unused-vars': 'off', 'no-use-before-define': 'off', }, }, { files: '{,.md/}*.vue', extends: ['plugin:vue/vue3-essential'], parserOptions: { parser: '@typescript-eslint/parser' }, }, { files: '*.spec.ts', extends: ['plugin:jest/recommended', 'plugin:jest/style'], rules: { 'array-bracket-spacing': 'off', 'import/no-extraneous-dependencies': 'off', 'jest/consistent-test-it': ['error', { fn: 'test' }], }, }, { files: '**/*.md', processor: 'markdown/markdown' }, { files: '**/*.md/*.{{ts,js}{,x},vue}', rules: { 'arrow-parens': 'off', 'import/no-named-as-default': 'off', 'import/no-unresolved': 'off', 'no-console': 'off', 'no-redeclare': 'off', 'no-shadow': 'off', 'no-undef': 'off', 'no-unused-vars': 'off', 'no-use-before-define': 'off', 'no-var': 'off', 'prefer-arrow-callback': 'off', }, }, ], } ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 14 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - pinned - security only: issues # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false exemptProjects: true ================================================ FILE: .github/workflows/publish.yml ================================================ on: push: branches: - main - next jobs: test: uses: ./.github/workflows/test.yml publish-npm: name: '📦 Build and Publish 🚀' needs: [test] runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' registry-url: https://registry.npmjs.org/ cache: yarn - name: ⚙ bootstrap run: 'npm run bootstrap && git fetch --tags' - name: 📦 build and publish 🚀 run: npx _release env: NODE_AUTH_TOKEN: ${{secrets.npm_token}} ================================================ FILE: .github/workflows/test.yml ================================================ on: pull_request: workflow_dispatch: workflow_call: jobs: test: name: '🧪 Test' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ianwalter/playwright-container@43940dfa7d309fe3569b9df407ae9e84dcbf2e7f - name: ⚙ bootstrap run: 'npm run bootstrap && npx _check_deps && npx _add_plugin_indexes' - name: 📐 types run: npx tsc -b -f - name: 🔍 lint run: npx _lint - name: 🧪 tests run: npm test ================================================ FILE: .gitignore ================================================ *.d.ts *.d.ts.map !interactjs/index.d.ts !shims.d.ts packages/@interactjs/**/index.ts packages/@interactjs/*/use/**/*.ts !packages/@interactjs/types/index.ts !packages/@interactjs/interact/index.ts !packages/@interactjs/interactjs/index.ts !packages/@interactjs/rebound/index.ts packages/**/*.js packages/**/*.js.map !packages/@interactjs/dev-tools/babel-plugin-prod.js node_modules !test/fixtures/**/node_modules dist .projectroot .env .envrc .nyc_output yarn-error.log .yarn-cache .pnpm-store npm-debug.log pnpm-debug.log coverage cc-test-reporter lerna-debug.log .vim .cjsescache ================================================ FILE: .husky/.gitignore ================================================ _ ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" yarn lint-staged ================================================ FILE: .npmignore ================================================ *.ts !*.d.ts *.map.* *.spec.ts *.spec.js dist/docs guide ================================================ FILE: .npmrc ================================================ link-workspace-packages = true shared-workspace-lockfile = true prefer-frozen-lockfile = true ================================================ FILE: .nvmrc ================================================ v20.10.0 ================================================ FILE: .prettierignore ================================================ dist coverage node_modules ================================================ FILE: .prettierrc.json ================================================ { "printWidth": 110, "semi": false, "singleQuote": true, "trailingComma": "all" } ================================================ FILE: .stylelintrc.cjs ================================================ module.exports = { extends: ['stylelint-config-standard', 'stylelint-config-recess-order', 'stylelint-config-css-modules'], ignoreFiles: ['dist/**/*', 'coverage/**/*'], } ================================================ FILE: .yarnrc ================================================ registry "https://registry.npmjs.org" ================================================ FILE: CHANGELOG.md ================================================ ## v1.10.27 - fix(types): fix issues with `skipLibCheck: false` ## v1.10.26 - fix: improve build; check output for ES2018 compatibility ## v1.10.25 - fix: bundle to ES5 syntax ## v1.10.24 - chore: generate api docs ## v1.10.23 - fix: transform nullish coalescing; fix symbol-tree build ## v1.10.22 - fix(actions/gesture): fix error when inertia is enabled for gestures #995 ## v1.10.21 - fix(actions/drop): fix regression with drop event targets #1016 ## v1.10.20 - fix(types): import plugins for module augmentations #933 ## v1.10.19 - fix(core/scope): remove duplicate Interactable super.unset - fix(utils/pointerExtend): skip all vendor-prefixed props. Close #978 ## v1.10.18 - fix(interact): remove types dependency - fix: set "type": "module" for scoped packages - fix(modifiers): allow toggling aspectRatio modifier during interaction - fix(types): import plugins for module augmentations - fix(interactjs): don't assign module.exports in esm package ## v1.10.17 - fixed missing typings when only the `@interactjs/interactjs` package is installed and imported - added index to vue package for installing with side effect import ## v1.10.16 - remove vue and react deps fron pro `@interactjs/interactjs` package ## v1.10.15 - fixed an issue with broken `@interactjs/types` #972 ## v1.10.14 - fixed an issue with iframes on Webkit #942. Thanks, @tulps, for PR #943 - fixed top-right and bottom-left resizing with aspectRatio with sub-modifiers #944. Thanks again, @tulps, for PR #963 - fixed typings for `@itneractjs/` scoped module packages #933 - added `doubletap.double === true` event prop for consistency with `tap.double` - fixed a bug with calling `interactable.unset()` in a `drop` listener #919 ## v1.10.13 - Added `.d.ts` files to all `@interactjs/*` packages ## v1.10.12 - fixed incorrect behaviour when `interactable.unset()` is called multiple times ## v1.10.11 - fixed incorrect "module" field in package.json https://github.com/taye/interact.js/issues/894#issuecomment-811046898 ## v1.10.10 - fixed issue with unresolved stub files #894 - fixed commonjs import of `interactjs` package ## v1.10.9 - improved support for SSR environments ## v1.10.8 - fixed imports of missing modules #891 ## v1.10.7 - correctly replace `process.env.npm_package_version` in min bundle #890 ## v1.10.6 - fix packaging error ## v1.10.5 - fix packaging error ## v1.10.4 - fix NPE in indexOfDeepestElement if first element has no parent #887 - improve babel-plugin-prod on windows #885 ## v1.10.3 - fixed issue with TS strict null checks #882 - fixed issue with type imports being emitted in JS modules #881 ## v1.10.2 - marked interact.{on,off} methods as deprecated ## v1.10.1 - fixed mouseButtons option typings #865 - removed plugin index module warnings ## v1.10.0 - changed production files extension from '.min.js' to '.prod.js' #857 - added experimental `@interactjs/dev-tools/babel-plugin-prod` babel plugin to change `@interactjs/*` imports to production versions - added `sideEffects` fields to package.json files ## v1.9.22 - fixed inertia issue with arbitrary plugin order #834 - fixed inertia regression #853 ## v1.9.21 - used findIndex polyfill to support 1E11 #852 - fixed issue where resize reflow increased element size #817 - fixed drop event order: fire `dropmove` after `dragenter` #841 and final drop events before `dragend` #842 - updated docs #844 #829 ## v1.9.20 - fixed ordering of plugins ## v1.9.19 - exposed `DropEvent` type ## v1.9.18 - fixed further issues with types ## v1.9.17 - fixed missing types for interactjs package ## v1.9.16 - fixed missing types for interactjs package ## v1.9.15 - fixed missing types for interactjs package ## v1.9.15 - fixed further regression breaking typescript builds #816 ## v1.9.14 - fixed regression breaking typescript builds #816 ## v1.9.13 - fixed regression breaking es5 compatibility of .min.js bundle #814 ## v1.9.12 - fixed regression breaking commonjs imports withotu .default ## v1.9.11 - fixed issue with missing width/height on rectChecker result - fixed resize checker with negative sizes - moved generated plugin use modules to @interactjs/_/{use/,}_/index.ts #800 - changed snap function args to provide interaction proxy - restored dev-tools helpers in development bundle ## v1.9.10 - fixed issue with uninitialized scope in non browser env #803 ## v1.9.9 - fixed typescript issue #807 ## v1.9.8 - fixed minified bundle #802 - fixed issue with removing delegated events #801 ## v1.9.7 - fixed typing issues ## v1.9.6 - improved package dependencies ## v1.9.5 - made `core` and `utils` packages dependencies of `interact` ## v1.9.4 - restored `@interactjs/*/use/*.js*` builds ## v1.9.2 - fixed imports within generated modules ## v1.9.1 - added `@interactjs/*/use/*.min.js` builds - fixed issue with webpack minifier #800 - fixed typescript issues ## v1.9.0 - added various `@interactjs/*/use` packages for simpler selective imports #800 - fixed endOnly modifiers without inertia ## v1.8.5 - fixed a but causing incorrect modifications after resuming inertia #790 ## v1.8.4 - fixed bug when calling interaction.move() from start event #791 ## v1.8.3 - fixed bug when calling interaction.move() from start event #791 - fixed invalid non-array argument spread types #789 - fixed missing typescript definition of some interactable methods #788 - disabled `.d.ts.map` files output since the `.ts` source files are not published - fixed typings for modifiers ## v1.8.2 - enabled `.d.ts.map` files output - added license field to @interactjs/interact package.json ## v1.8.1 - fixed an issue causing flickering a cursor on Firefox for Windows #781 ## v1.8.0 Changes from prerelease versions listed below. See https://github.com/taye/interact.js/projects/4#column-7093512 for a list of issues and pull requests. ## v1.8.0-rc.3 - fixed incorrect publish ## v1.8.0-rc.2 - refactoring ## v1.8.0-rc.1 - fixed `interact.snappers.grid` arg typings (https://twitter.com/ksumarine/status/1204457347856424960) - removed "?" from definitions for interact.{modifiers,snappers,createSnapGrid} ## v1.8.0-rc.0 - fixed `modifiers.restrictSize` #779 - fixed option types in typescript and fixed devTools options #776 ## v1.8.0-alpha.7 - reverted to typescript@3.6 to avoid backwards compatibility issues #775 ## v1.8.0-alpha.6 - fixed dev scripts ## v1.8.0-alpha.5 - moved `interact.dynamicDrop` definition in order to avoid compilation errors ## v1.8.0-alpha.4 - added `main` field to interactjs package.json #774 - removed baseUrl from project tsconfig to avoid relative imports in generated declarations ## v1.8.0-alpha.3 - added missing typescript declaration files ## v1.8.0-alpha.2 - used non relative imports in .ts files with correct config for babel-plugin-bare-import-rewrite ## v1.8.0-alpha.1 - added `event.modifiers` array #772 ## v1.8.0-alpha.0 - added `aspectRatio` modifier #638 ## v1.7.4 - fixed `interact.snappers.grid` arg typings (https://twitter.com/ksumarine/status/1204457347856424960) - removed "?" from definitions for interact.{modifiers,snappers,createSnapGrid} ## v1.7.3 - fixed interactjs package main and browser fields #774 - reverted to typescript@3.6 to avoid backwards compatibility issues #775 ## v1.7.2 - fixed typescript definition files #771 ## v1.7.1 - reorganized modules for esnext resolution ## v1.7.0 - fixed hold repeat `event.count` - added esnext js builds #769 ## v1.6.3 - fixed issue with inertia resume with `endOnly: false` #765 ## v1.6.2 - @mlucool added license field to package.json of sub modules #755 - added `rect`, `deltaRect` and `edges` to resizestart and resizeend events #754 ## v1.6.1 - fixed resize without invert ## v1.6.0 - avoided accessing deprecated event.mozPressure #751 - changed typings to use `HTMLElement | SVGElement` for `event.target` #747 - added `interacting` arg to cursorChecker #739 - added zIndex compare for sibling dropzones ## v1.5.4 - fixed broken modifiers #746 ## v1.5.3 - fixed issues with old modifiers API ## v1.5.2 - fixed null restriction issue #737 - improved typings for modifiers ## v1.5.1 - fixed typing issues #738 ## v1.5.0 - added `cursorChecker` option for drag and resize #736 - allowed restrictions larger than the target element #735 - added `interact.modifiers.restrictRect` with pre-set elementRect #735 ## v1.4.14 - fixed issue with string restriction values that don't resolve to a rect #731 - changed plugin order so that `pointer-events` is installed before `inertia` ## v1.4.13 - fixed restrictSize min and max function restrictions ## v1.4.12 - fixed errors from calling `interaction.stop()` in start event #725 ## v1.4.11 - fixed hold events #730 ## v1.4.10 - fixed regression of preventing native drag behaviour #729 ## v1.4.9 - fixed modifiers with inertia action-resume #728 - fixed docs for snap grid limits #717 ## v1.4.8 - fixed exports in generated typings #727 ## v1.4.7 - fixed exports in generated typings #726 ## v1.4.6 - fixed pointerEvents currentTarget ## v1.4.5 - @0xflotus fixed typos in docs #724 - fixed error on iOS #682 ## v1.4.4 - fixed an issue with interactions lingering on removed elements #723 ## v1.4.3 - destroy only relevant interactions on interactable.unset() ## v1.4.2 - @jf-m fixed memory leaks and a bug on interactions stop [PR #715](https://github.com/taye/interact.js/pull/715) - fixed dropzones in shadow DOM [PR #722](https://github.com/taye/interact.js/pull/722) ## v1.4.1 - fixed scripts to run bundle optimizations and fix issues with browserify # v1.4.0 Most notablly: - `interactable.reflow(action)` to re-run modifiers, drop, etc [PR #610](https://github.com/taye/interact.js/pull/610) - `dropEvent.reject()` [PR #613](https://github.com/taye/interact.js/pull/613) - snapEdges modifier [PR #620](https://github.com/taye/interact.js/pull/620) - per-action modifiers array [PR #625](https://github.com/taye/interact.js/pull/625) - autoStart set cursor on both target and <html> [PR #639](https://github.com/taye/interact.js/pull/639) - inertia: rename resume event to `${action}resume` - `interactable.reflow(action)` to re-run modifiers, drop, etc [PR #610](https://github.com/taye/interact.js/pull/610) - added `options.listeners` array/object for actions - `snapEdges` modifier [PR #620](https://github.com/taye/interact.js/pull/620) - fixed iOS preventDefault passive event issue ([issue #631](https://github.com/taye/interact.js/issues/631)) - added `console.warn` messages for common, easily detected issues - improved docs - various fixes Full list of [changes on Github](https://github.com/taye/interact.js/compare/1.3.4...v1.4.0). ## v1.3.3 - fixed issues with action options ([PR #567](https://github.com/taye/interact.js/pull/567), [issue #570](https://github.com/taye/interact.js/issues/570)) ## v1.3.2 - fixed iOS preventDefault passive event issue ([issue #561](https://github.com/taye/interact.js/issues/561)) ## v1.3.1 - allowed calling `draggable.unset()` during `dragend` and `drop` event listeners ([issue #560](https://github.com/taye/interact.js/issues/560)) - allowed snap to be enabled with falsey targets value [issue #562](https://github.com/taye/interact.js/issues/562) ## v1.3.0 Most notably: - changed the npm and bower package names to "interactjs" ([issue #399](https://github.com/taye/interact.js/issues/399) - major refactor with [PR #231](https://github.com/taye/interact.js/pull/231). - removed deprecated methods: - `Interactable`: `squareResize`, `snap`, `restrict`, `inertia`, `autoScroll`, `accept` - `interact`: `enabbleDragging`, `enableResizing`, `enableGesturing`, `margin` - new `hold` option for starting actions - new `interaction.end()` method ([df963b0](https://github.com/taye/interact.js/commit/df963b0)) - `snap.offset` `self` option ([issue #204](https://github.com/taye/interact.js/issues/204/#issuecomment-154879052)) - `interaction.doMove()` ([3489ee1](https://github.com/taye/interact.js/commit/3489ee1)) ([c5c658a](https://github.com/taye/interact.js/commit/c5c658a)) - snap grid limits ([d549672](https://github.com/taye/interact.js/commit/d549672)) - improved iframe support ([PR #313](https://github.com/taye/interact.js/pull/313)) - `actionend` event dx/dy are now `0`, not the difference between start and end coords ([cbfaf00](https://github.com/taye/interact.js/commit/cbfaf00)) - replaced drag `axis` option with `startAxis` and `lockAxis` - added pointerEvents options: - `holdDuration` ([1c58f92](https://github.com/taye/interact.js/commit/1c58f927)), - `ignoreFrom` and `allowFrom` ([6cbaad6](https://github.com/taye/interact.js/commit/6cbaad6d)) - `origin` ([7823bb9](https://github.com/taye/interact.js/commit/7823bb95)) - action events set with action method options (eg. `target.draggable({onmove})` are removed when that action is disabled with a method call ([cca4e26](https://github.com/taye/interact.js/commit/cca4e260)) - `context` option now works for Element targets ([8f64a7a](https://github.com/taye/interact.js/commit/8f64a7a4)) - added an action `mouseButtons` option and allowed actions only with the left mouse button by default ([54ebdc3](https://github.com/taye/interact.js/commit/54ebdc3e)) - added repeating `hold` events ([fe11a8e](https://github.com/taye/interact.js/commit/fe11a8e5)) - fixed `Interactable.off` ([PR #477](https://github.com/taye/interact.js/pull/477)) - added `restrictEdges`, `restrictSize` and `snapSize` resize modifiers ([PR #455](https://github.com/taye/interact.js/pull/455)) Full list of [changes on Github](https://github.com/taye/interact.js/compare/v1.2.6...v1.3.0). ## 1.2.6 ### resize.preserveAspectRatio ```javascript interact(target).resizable({ preserveAspectRatio: true }) ``` See [PR #260](https://github.com/taye/interact.js/pull/260). ### Deprecated - `interact.margin(number)` - Use `interact(target).resizable({ margin: number });` instead ### Fixed - incorrect coordinates of the first movement of every action ([5e5a040](https://github.com/taye/interact.js/commit/5e5a040)) - warning about deprecated "webkitForce" event property ([0943290](https://github.com/taye/interact.js/commit/0943290)) - bugs with multiple concurrent interactions ([ed53aee](http://github.com/taye/interact.js/commit/ed53aee)) - iPad 1, iOS 5.1.1 error "undefined is not a function" when autoScroll is set to true ([PR #194](https://github.com/taye/interact.js/pull/194)) Full list of [changes on Github](https://github.com/taye/interact.js/compare/v1.2.5...v1.2.6) ## 1.2.5 ### Changed parameters to actionChecker and drop.checker - Added `event` as the first argument to actionCheckers. See commit [88dc583](https://github.com/taye/interact.js/commit/88dc583) - Added `dragEvent` as the first parameter to drop.checker functions. See commits [16d74d4](https://github.com/taye/interact.js/commit/16d74d4) and [d0c4b69](https://github.com/taye/interact.js/commit/d0c4b69) ### Deprecated methods interactable.accept - instead, use: ```javascript interact(target).dropzone({ accept: stringOrElement }) ``` interactable.dropChecker - instead, use: ```javascript interact(target).dropzone({ checker: function () {} }) ``` ### Added resize.margin See https://github.com/taye/interact.js/issues/166#issuecomment-91234390 ### Fixes - touch coords on Presto Opera Mobile - see commits [886e54c](https://github.com/taye/interact.js/commit/886e54c) and [5a3a850](https://github.com/taye/interact.js/commit/5a3a850) - bug with multiple pointers - see commit [64882d3](https://github.com/taye/interact.js/commit/64882d3) - accessing certain recently deprecated event properties in Blink - see commits [e91fbc6](https://github.com/taye/interact.js/commit/e91fbc6) and [195cfe9](https://github.com/taye/interact.js/commit/195cfe9) - dropzones with `accept: 'pointer'` in scrolled pages on iOS6 and lower - see commit [0b94aac](https://github.com/taye/interact.js/commit/0b94aac) - setting styleCursor through Interactable options object - see [PR #270](https://github.com/taye/interact.js/pull/270) - one missed interaction element on stop triggered - see [PR #258](https://github.com/taye/interact.js/pull/258) - pointer dt on touchscreen devices - see [PR #215](https://github.com/taye/interact.js/pull/215) - autoScroll with containers with fixed position - see commit [3635840](https://github.com/taye/interact.js/commit/3635840) - autoScroll for mobile - see #180 - preventDefault - see commits [1984c80](https://github.com/taye/interact.js/commit/1984c80) and [6913959](https://github.com/taye/interact.js/commit/6913959) - occasional error - see [issue #183](https://github.com/taye/interact.js/issue/183) - Interactable#unset - see [PR #178](https://github.com/taye/interact.js/pull/178) - coords of start event after manual start - see commit [fec73b2](https://github.com/taye/interact.js/commit/fec73b2) - bug with touch and selector interactables - see commit [d8df3de](https://github.com/taye/interact.js/commit/d8df3de) - touch doubletap bug - see [273f461](https://github.com/taye/interact.js/commit/273f461) - event x0/y0 with origin - see [PR #167](https://github.com/taye/interact.js/pull/167) ## 1.2.4 ### Resizing from all edges With the new [resize edges API](https://github.com/taye/interact.js/pull/145), you can resize from the top and left edges of an element in addition to the bottom and right. It also allows you to specify CSS selectors, regions or elements as the resize handles. ### Better `dropChecker` arguments The arguments to `dropChecker` functions have been expanded to include the value of the default drop check and some other useful objects. See [PR 161](https://github.com/taye/interact.js/pull/161) ### Improved `preventDefault('auto')` If manuanStart is `true`, default prevention will happen only while interacting. Related to [Issue 138](https://github.com/taye/interact.js/issues/138). ### Fixed inaccurate snapping This removes a small inaccuracy when snapping with one or more `relativeOffsets`. ### Fixed bugs with multiple pointers ## 1.2.3 ### ShadowDOM Basic support for ShadowDOM was implemented in [PR 143](https://github.com/taye/interact.js/pull/143) ### Fixed some issues with events Fixed Interactable#on({ type: listener }). b8a5e89 Added a `double` property to tap events. `tap.double === true` if the tap will be followed by a `doubletap` event. See [issue 155](https://github.com/taye/interact.js/issues/155#issuecomment-71202352). Fixed [issue 150](https://github.com/taye/interact.js/issues/150). ## 1.2.2 ### Fixed DOM event removal See [issue 149](https://github.com/taye/interact.js/issues/149). ## 1.2.1 ### Fixed Gestures Gestures were completely [broken in v1.2.0](https://github.com/taye/interact.js/issues/146). They're fixed now. ### Restriction Fixed restriction to an element when the element doesn't have a rect (`display: none`, not in DOM, etc.). [Issue 144](https://github.com/taye/interact.js/issues/144). ## 1.2.0 ### Multiple interactions Multiple interactions have been enabled by default. For example: ```javascript interact('.drag-element').draggable({ enabled: true, // max : Infinity, // default // maxPerElement: 1, // default }) ``` will allow multiple `.drag-element` to be dragged simultaneously without having to explicitly set max: integerGreaterThan1. The default `maxPerElement` value is still 1 so only one drag would be able to happen on each `.drag-element` unless the `maxPerElement` is changed. If you don't want multiple interactions, call `interact.maxInteractions(1)`. ### Snapping #### Unified snap modes Snap modes have been [unified](https://github.com/taye/interact.js/pull/127). A `targets` array now holds all the snap objects and functions for snapping. `interact.createSnapGrid(gridObject)` returns a function that snaps to the dimensions of the given grid. #### `relativePoints` and `origin` ```javascript interact(target).draggable({ snap: { targets: [{ x: 300, y: 300 }], relativePoints: [ { x: 0, y: 0 }, // snap relative to the top left of the element { x: 1, y: 1 }, // and also to the bottom right ], // offset the snap target coordinates // can be an object with x/y or 'startCoords' offset: { x: 50, y: 50 }, }, }) ``` #### snap function interaction arg The current `Interaction` is now passed as the third parameter to snap functions. ```js interact(target).draggable({ snap: { targets: [ function (x, y, interaction) { if (!interaction.dropTarget) { return { x: 0, y: 0 } } }, ], }, }) ``` #### snap.relativePoints and offset The `snap.relativePoints` array succeeds the snap.elementOriign object. But backwards compatibility with `elementOrigin` and the old snapping interface is maintained. `snap.offset` lets you offset all snap target coords. See [this PR](https://github.com/taye/interact.js/pull/133) for more info. #### slight change to snap range calculation Snapping now occurs if the distance to the snap target is [less than or equal](https://github.com/taye/interact.js/commit/430c28c) to the target's range. ### Inertia `inertia.zeroResumeDelta` is now `true` by default. ### Per-action settings Snap, restrict, inertia, autoScroll can be different for drag, restrict and gesture. See [PR 115](https://github.com/taye/interact.js/pull/115). Methods for these settings on the `interact` object (`interact.snap()`, `interact.autoScroll()`, etc.) have been removed. ### Space-separated string and array event list and eventType:listener object ```javascript function logEventType(event) { console.log(event.type, event.target) } interact(target).on('down tap dragstart gestureend', logEventType) interact(target).on(['move', 'resizestart'], logEventType) interact(target).on({ dragmove: logEvent, keydown: logEvent, }) ``` ### Interactable actionChecker The expected return value from an action checker has changed from a string to an object. The object should have a `name` and can also have an `axis` property. For example, to resize horizontally: ```javascript interact(target) .resizeable(true) .actionChecker(function (pointer, defaultAction, interactable, element) { return { name: 'resize', axis: 'x', } }) ``` ### Plain drop event objects All drop-related events are [now plain objects](https://github.com/taye/interact.js/issues/122). The related drag events are referenced in their `dragEvent` property. ### Interactable.preventDefault('always' || 'never' || 'auto') The method takes one of the above string values. It will still accept `true`/`false` parameters which are changed to `'always'`/`'never'`. ## 1.1.3 ### Better Events Adding a function as a listener for an InteractEvent or pointerEvent type multiple times will cause that function to be fired multiple times for the event. Previously, adding the event type + function combination again had no effect. Added new event types [down, move, up, cancel, hold](https://github.com/taye/interact.js/pull/101). Tap and doubletap with multiple pointers was improved. Added a workaround for IE8's unusual [dblclick event sequence](http://www.quirksmode.org/dom/events/click.html) so that doubletap events are fired. Fixed a [tapping issue](https://github.com/taye/interact.js/issues/104) on Windows Phone/RT. Fixed a bug that caused the origins of all elements with tap listeners to be subtracted successively as a tap event propagated. [Fixed delegated events](https://github.com/taye/interact.js/commit/e972154) when different contexts have been used. ### iFrames [Added basic support](https://github.com/taye/interact.js/pull/98) for sharing one instance of interact.js between multiplie windows/frames. There are still some issues. ================================================ FILE: ISSUE_TEMPLATE.md ================================================ If you have questions about the [API](http://interactjs.io/api) that aren't answered in the [docs](http://interactjs.io/docs) or [FAQ](http://interactjs.io/docs/faq), try asking in the [Gitter chatroom](https://gitter.im/taye/interact.js) or on [Stackoverflow](https://stackoverflow.com/questions/tagged/interact.js). If you've found something that looks like a bug, include a link to a minimal demo on [JSFilddle](https://jsfiddle.net), [Codepen](https://codepen.io) with instructions to reproduce the bug with and roughly follow the following issue description format: ### Expected behavior Tell us what should happen ### Actual behavior Tell us what happens instead ### System configuration **interact.js version**: **Browser name and version**: **Operating System**: ================================================ FILE: LICENSE ================================================ Copyright (c) 2012-present Taye Adeyemi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: PULL_REQUEST_TEMPLATE.md ================================================ Make sure to include tests in your pull request. ================================================ FILE: README.md ================================================ interact.js

JavaScript drag and drop, resizing and multi-touch gestures with inertia and snapping for modern browsers (and also IE9+).

Gitter jsDelivr Build Status

Features include: - **inertia** and **snapping** - **multi-touch**, simultaneous interactions - cross browser and device, supporting the **desktop and mobile** versions of Chrome, Firefox and Opera as well as **Internet Explorer 9+** - interaction with [**SVG**](http://interactjs.io/#use_in_svg_files) elements - being **standalone and customizable** - **not modifying the DOM** except to change the cursor (but you can disable that) ## Installation - [npm](https://www.npmjs.org/): `npm install interactjs` - [jsDelivr CDN](https://cdn.jsdelivr.net/npm/interactjs/): `` - [unpkg CDN](https://unpkg.com/interactjs/): `` - [Rails 5.1+](https://rubyonrails.org/): 1. `yarn add interactjs` 2. `//= require interactjs/interact` - [Webjars SBT/Play 2](https://www.webjars.org/): `libraryDependencies ++= Seq("org.webjars.npm" % "interactjs" % version)` ### Typescript definitions The project is written in Typescript and the npm package includes the type definitions, but if you need the typings alone, you can install them with: ``` npm install --save-dev @interactjs/types ``` ## Documentation http://interactjs.io/docs ## Example ```javascript var pixelSize = 16; interact('.rainbow-pixel-canvas') .origin('self') .draggable({ modifiers: [ interact.modifiers.snap({ // snap to the corners of a grid targets: [ interact.snappers.grid({ x: pixelSize, y: pixelSize }), ], }) ], listeners: { // draw colored squares on move move: function (event) { var context = event.target.getContext('2d'), // calculate the angle of the drag direction dragAngle = 180 * Math.atan2(event.dx, event.dy) / Math.PI; // set color based on drag angle and speed context.fillStyle = 'hsl(' + dragAngle + ', 86%, ' + (30 + Math.min(event.speed / 1000, 1) * 50) + '%)'; // draw squares context.fillRect(event.pageX - pixelSize / 2, event.pageY - pixelSize / 2, pixelSize, pixelSize); } } }) // clear the canvas on doubletap .on('doubletap', function (event) { var context = event.target.getContext('2d'); context.clearRect(0, 0, context.canvas.width, context.canvas.height); }); function resizeCanvases () { [].forEach.call(document.querySelectorAll('.rainbow-pixel-canvas'), function (canvas) { canvas.width = document.body.clientWidth; canvas.height = window.innerHeight * 0.7; }); } // interact.js can also add DOM event listeners interact(document).on('DOMContentLoaded', resizeCanvases); interact(window).on('resize', resizeCanvases); ``` See the above code in action at https://codepen.io/taye/pen/tCKAm ## License interact.js is released under the [MIT License](http://taye.mit-license.org). [ijs-twitter]: https://twitter.com/interactjs [upcoming-changes]: https://github.com/taye/interact.js/blob/main/CHANGELOG.md#upcoming-changes ================================================ FILE: babel.config.cjs ================================================ module.exports = { presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], } ================================================ FILE: bin/_add_plugin_indexes ================================================ #!/usr/bin/env node require('../scripts/bin/add_plugin_indexes') ================================================ FILE: bin/_bundle ================================================ #!/usr/bin/env node require('../scripts/bin/bundle') ================================================ FILE: bin/_check_deps ================================================ #!/usr/bin/env node require('../scripts/bin/_check_deps') ================================================ FILE: bin/_clean ================================================ #!/usr/bin/env node require('../scripts/bin/clean') ================================================ FILE: bin/_link ================================================ #!/bin/bash ROOT=$(dirname $(dirname $(readlink -f $0))) if [ -z "$ROOT" ] then ROOT=$(dirname $(dirname $0)) fi modules_scope_dir=node_modules/@interactjs modules_bin_dir=node_modules/.bin mkdir -p $modules_scope_dir $modules_bin_dir rm $modules_scope_dir/* 2> /dev/null # link _dev package ln -sf $ROOT $modules_scope_dir/_dev # link all scoped packages from CWD for package in $(cd packages/@interactjs && ls -d *); do ln -sf ../../packages/@interactjs/$package $modules_scope_dir done # link all packages from this repo for package in $(cd $ROOT/packages/@interactjs && ls -d *); do ln -sf $ROOT/packages/@interactjs/$package $modules_scope_dir done # link all bins from this repo and from CWD cd node_modules/.bin && ln -sf $ROOT/bin/* ../../bin/* . ================================================ FILE: bin/_lint ================================================ #!/usr/bin/env node require('../scripts/bin/lint') ================================================ FILE: bin/_release ================================================ #!/usr/bin/env node require('../scripts/bin/release') ================================================ FILE: bin/_types ================================================ #!/usr/bin/env node require('../scripts/bin/types') ================================================ FILE: bin/_version ================================================ #!/usr/bin/env node require('../scripts/bin/version') ================================================ FILE: bundle.rollup.config.cjs ================================================ /* eslint-disable import/no-extraneous-dependencies */ const { resolve } = require('path') const babel = require('@rollup/plugin-babel') const commonjs = require('@rollup/plugin-commonjs') const nodeResolve = require('@rollup/plugin-node-resolve') const replace = require('@rollup/plugin-replace') const terser = require('@rollup/plugin-terser') const { defineConfig } = require('rollup') const headers = require('./scripts/headers') const { extendBabelOptions, getModuleDirectories, isPro } = require('./scripts/utils') const globals = { react: 'React', vue: 'Vue', } const external = Object.keys(globals) const INPUT_EXTENSIONS = ['.ts', '.tsx', '.vue'] module.exports = defineConfig(async () => { const variations = [ { env: { NODE_ENV: 'development' }, ext: '.js', minify: isPro }, { env: { NODE_ENV: 'production' }, ext: '.min.js', minify: true }, ] return variations.map(({ minify, ext, env }) => { const babelConfig = extendBabelOptions({ babelrc: false, configFile: false, browserslistConfigFile: false, targets: { ie: 9 }, babelHelpers: 'bundled', skipPreflightCheck: true, extensions: INPUT_EXTENSIONS, plugins: [[require.resolve('@babel/plugin-transform-runtime'), { helpers: false, regenerator: true }]], }) return defineConfig({ input: resolve(__dirname, 'packages', 'interactjs', 'index.ts'), external, plugins: [ nodeResolve({ modulePaths: getModuleDirectories(), extensions: INPUT_EXTENSIONS, }), commonjs({ include: '**/node_modules/{rebound,symbol-tree}/**' }), babel(babelConfig), replace({ preventAssignment: true, values: Object.entries({ npm_package_version: process.env.npm_package_version, IJS_BUNDLE: '1', ...env, }).reduce((acc, [key, value]) => { acc[`process.env.${key}`] = JSON.stringify(value) return acc }, {}), }), minify && terser({ module: false, mangle: true, compress: { ecma: 5, unsafe: true, unsafe_Function: true, unsafe_arrows: false, unsafe_methods: true, }, format: { preamble: headers?.min, }, }), ], context: 'window', moduleContext: 'window', output: { file: resolve(__dirname, 'packages', 'interactjs', 'dist', `interact${ext}`), format: 'umd', name: 'interact', banner: minify ? headers.min : headers.raw, minifyInternalExports: true, inlineDynamicImports: true, sourcemap: true, globals, }, }) }) }) ================================================ FILE: docs/action-options.md ================================================ --- title: Action Options --- The `Interactable` methods `draggable()`, `resizable()` and `gesturable()` are used to enable and configure actions for target elements. They all have some common options as well as some action-specific options and event properties. Drag, resizem and gesture interactions fire `InteractEvent`s which have the following properties common to all action types: | InteractEvent property | Description | | ------------------------ | ------------------------------------------------ | | `target` | The element that is being interacted with | | `interactable` | The Interactable that is being interacted with | | `interaction` | The Interaction that the event belongs to | | `x0`, `y0` | Page x and y coordinates of the starting event | | `clientX0`, `clientY0` | Client x and y coordinates of the starting event | | `dx`, `dy` | Change in coordinates of the mouse/touch | | `velocityX`, `velocityY` | The Velocity of the pointer | | `speed` | The speed of the pointer | | `timeStamp` | The time of creation of the event object | ## Common Action Options The Interactable methods `draggable`, `resizable` and `gesturable` take either `true` or `false` to simply allow/disallow the action or an object with properties to change certain settings. ### `max` `max` is used to limit the number of concurrent interactions that can target an interactable. By default, any number of interactions can target an interactable. ### `maxPerElement` By default only 1 interaction can target the same interactable+element combination. If you want to allow multiple interactions on the same target element, set the `maxPerElement` property of your object to a value `>= 2`. ### `manualStart` If this is changed to `true` then drag, resize and gesture actions will have to be started with a call to [`Interaction#start`][interaction-start] as the usual `down`, `move`, `start`... sequence will not start an action. See [auto-start](/docs/auto-start). ### `hold` The action will start after the pointer is held down for the given number of milliseconds. ### `inertia` Change inertia settings for drag, and resize. See [docs/inertia](/docs/inertia). ### `styleCursor` If the [auto-start](/docs/auto-start) feature is enabled, interact will style the cursor of draggable and resizable elements as you hover over them. ```js interact(target).styleCursor(false) ``` To disable this for all actions, set the `styleCursor` option to `false` ### `cursorChecker` ```js interact(target) .resizable({ edges: { left: true, right: true }, cursorChecker (action, interactable, element, interacting) { // the library uses biderectional arrows <-> by default, // but we want specific arrows (<- or ->) for each diriection if (action.edges.left) { return 'w-resize' } if (action.edges.right) { return 'e-resize' } }, }) .draggable({ cursorChecker () { // don't set a cursor for drag actions return null }, }) ``` You can disable default cursors with `interact(target).styleCursor(false)`, but that will disable cursor styling for all actions. To disable or change the cursor for each action, you can set a `cursorChecker` function which takes info about the current interaction and returns the CSS cursor value to set on the target element. ### `autoScroll` ```javascript interact(element) .draggable({ autoScroll: true, }) .resizable({ autoScroll: { container: document.body, margin: 50, distance: 5, interval: 10, speed: 300, } }) ``` Scroll a container (`window` or an HTMLElement) when a drag or resize move happens at the edge of the container. ### `allowFrom` (handle) ```html
Content
``` ```javascript interact('.movable-box') .draggable({ allowFrom: '.drag-handle', }) .resizable({ allowFrom: '.resize-handle', }) .pointerEvents({ allowFrom: '*', }) ``` The `allowFrom` option lets you specify a target CSS selector or Element which must be the target of the pointer down event in order for the action to start. This option available for drag, resize and gesture, as well as `pointerEvents` (down, move, hold, etc.). Using the `allowFrom` option, you may specify handles for each action separately and for all your pointerEvents listeners. The `allowFrom` elements **must** be children of the target interactable element. {.notice .info} ### `ignoreFrom` ```html

Selectable text

Should not fire tap, hold, etc. events
``` ```javascript var movable = document.querySelector('#movable-box') interact(movable) .draggable({ ignoreFrom: '.content', onmove: function (event) { /* ... */ } }) .pointerEvents({ ignoreFrom: '[no-pointer-event]', }) .on('tap', function (event) { }) ``` The compliment to `allowFrom`, `ignoreFrom` lets you specify elements within your target with which to avoid starting actions. This is useful when certain elements need to maintain default behavior when interacted with. For example, dragging around a text/contentEditable, by wrapping this object with a draggable element and ignoring the editable content you maintain the ability to highlight text without moving the element. ### `enabled` Enable the action for the Interactable. If the options object has no `enabled` property or the property value is `true` then the action is enabled. If `enabled` is false, the action is disabled. [interaction-start]: /docs/auto-start ================================================ FILE: docs/auto-start.md ================================================ --- title: 'AutoStart (manualStart: false)' --- The [pre-bundled](/docs/installation) package includes the `auto-start` plugin which will start interactions when the pointer goes down and then moves on enabled target elements. You can disable this for an action by setting the `manualStart` option to `true`. ```js interact(target) .draggable({ manualStart: true, }) .on('doubletap', function (event) { var interaction = event.interaction if (!interaction.interacting()) { interaction.start( { name: 'drag' }, event.interactable, event.currentTarget, ) } }) ``` With `manualStart: true`, you will need to start the action from a pointer event listener by calling `event.interaction.start(actionInfo)`. Because the library no longer decides when to start actions, the cursor will not be set automatically. ================================================ FILE: docs/draggable.md ================================================ --- title: Draggable --- Dragging is the simplest action interact.js provides. To make an element draggable, create an interactable with your desired target then call the `draggable` method with the options that you need. ```html
Draggable Element
``` ```css .draggable { touch-action: none; user-select: none; } ``` ```js const position = { x: 0, y: 0 } interact('.draggable').draggable({ listeners: { start (event) { console.log(event.type, event.target) }, move (event) { position.x += event.dx position.y += event.dy event.target.style.transform = `translate(${position.x}px, ${position.y}px)` }, }, }) ``` In addition to the common [`InteractEvent`](/docs/events#interactevents) properties, `dragmove` events also have: | Drag event property | Description | | ------------------- | ------------------------------------------------- | | `dragEnter` | The dropzone this Interactable was dragged over | | `dragLeave` | The dropzone this Interactable was dragged out of | Remember to use CSS `touch-action: none` to prevent the browser from panning when the user drags with a touch pointer, and `user-select: none` to disable text selection. {.notice .info} ## `lockAxis` and `startAxis` ```javascript // lock the drag to the starting direction interact(singleAxisTarget).draggable({ startAxis: 'xy' lockAxis: 'start' }); // only drag if the drag was started horizontally interact(horizontalTarget).draggable({ startAxis: 'x' lockAxis: 'x' }); ``` There are two options for controlling the axis of drag actions: `startAxis` and `lockAxis`. `startAxis` sets the direction that the initial movement must be in for the action to start. Use `'x'` to require the user to start dragging horizontally or `'y'` to start dragging vertically. `lockAxis` causes the drag events to change only in the given axis. If a value of `'start'` is used, then the drag will be locked to the starting direction. ================================================ FILE: docs/dropzone.md ================================================ --- title: Dropzone --- Dropzones define elements that draggable targets can be "dropped" into and which elements will be accepted. Like with drag events, drop events don't modify the DOM to re-parent elements. You will have to do this in your own event listeners if you need this. ```javascript interact(dropTarget) .dropzone({ ondrop: function (event) { alert(event.relatedTarget.id + ' was dropped into ' + event.target.id) } }) .on('dropactivate', function (event) { event.target.classList.add('drop-activated') }) ``` ## Dropzone Events Dropzone events are plain objects with the following properties: | Property | Description | | --------------- | ----------------------------------------------- | | `target` | The dropzone element | | `dropzone` | The dropzone Interactable | | `relatedTarget` | The element that's being dragged | | `draggable` | The Interactable that's being dragged | | `dragEvent` | The related drag event – `drag{start,move,end}` | | `timeStamp` | Time of the event | | `type` | The event type | ```javascript interact('.dropzone').dropzone({ accept: '.drag0, .drag1', }); ``` ## `accept` The dropzone `accept` option is a CSS selector or element which must match the dragged element in order for drop events to be fired. ```javascript interact(target).dropzone({ overlap: 0.25 }); ``` The `overlap` option sets how drops are checked for. The allowed values are: - `'pointer'` – the pointer must be over the dropzone (default) - `'center'` – the draggable element's center must be over the dropzone - a number from 0-1 which is the (intersection area) / (draggable area). e.g. `0.5` for drop to happen when half of the area of the draggable is over the dropzone ## `checker` The `checker` option is a function that you set to additionally check if a dragged element can be dropped into a dropzone. ```javascript interact(target).dropzone({ checker: function ( dragEvent, // related dragmove or dragend event, // Touch, Pointer or Mouse Event dropped, // bool default checker result dropzone, // dropzone Interactable dropzoneElement, // dropzone element draggable, // draggable Interactable draggableElement // draggable element ) { // only allow drops into empty dropzone elements return dropped && !dropElement.hasChildNodes(); } }); ``` The checker function takes the following arguments: | Arg | Description | | ------------------ | --------------------------------------------------- | | `dragEvent` | related dragmove or dragend event | | `event` | The user move/up/end Event related to the dragEvent | | `dropped` | The value from the default drop checker | | `dropzone` | The dropzone interactable | | `dropElement` | The dropzone element | | `draggable` | The Interactable being dragged | | `draggableElement` | The actual element that's being dragged | ================================================ FILE: docs/events.md ================================================ --- title: Events --- ## InteractEvents ```html
Drag, resize, or perform a multi-touch gesture
``` ```css .target { display: inline-block; min-height: 3rem; background-color: #29e; color: white; padding: 1rem; border-radius: 0.75rem; } ``` ```javascript function listener(event) { event.target.textContent = `${event.type} at ${event.pageX}, ${event.pageY}` } interact(target) .on('dragstart', listener) .on('dragmove dragend', listener) .on(['resizemove', 'resizeend'], listener) .on({ gesturestart: listener, gestureend: listener, }) interact(target).draggable({ onstart: listener, onmove: listener, onend: listener, }) interact(target).resizable({ listeners: [ { start: function (event) { console.log(event.type, event.pageX, event.pageY) }, }, ], }) ``` `InteractEvent`s are fired for different actions. The event types include: - Draggable: `dragstart`, `dragmove`, `draginertiastart`, `dragend` - Resizable: `resizestart`, `resizemove`, `resizeinertiastart`, `resizeend` - Gesturable: `gesturestart`, `gesturemove`, `gestureend` To respond to `InteractEvent`s, you must add listeners for the event types on an interactable that's configured for that action. The event object that was created is passed to these functions as the first and only parameter. `InteractEvent` properties include the usual properties of mouse/touch events such as `pageX/Y`, `clientX/Y`, modifier keys etc. but also some properties providing information about the change in coordinates and event specific data. The table below displays all of these events. ### Common | Property | Description | | ------------------------ | ------------------------------------------------ | | `target` | The element that is being interacted with | | `interactable` | The Interactable that is being interacted with | | `interaction` | The Interaction that the event belongs to | | `x0`, `y0` | Page x and y coordinates of the starting event | | `clientX0`, `clientY0` | Client x and y coordinates of the starting event | | `dx`, `dy` | Change in coordinates of the mouse/touch | | `velocityX`, `velocityY` | The Velocity of the pointer | | `speed` | The speed of the pointer | | `timeStamp` | The time of creation of the event object | ### Drag | Property | Description | | ------------ | ------------------------------------------------- | | **dragmove** | | | `dragEnter` | The dropzone this Interactable was dragged over | | `dragLeave` | The dropzone this Interactable was dragged out of | ### Resize | Property | Description | | ----------- | ------------------------------------------------- | | `edges` | The edges of the element that are being changed | | `rect` | An object with the new dimensions of the target | | `deltaRect` | The change in dimensions since the previous event | ### Gesture | Property | Description | | ---------- | --------------------------------------------------------------------------------- | | `distance` | The distance between the event's first two touches | | `angle` | The angle of the line made by the two touches | | `da` | The change in angle since previous event | | `scale` | The ratio of the distance of the start event to the distance of the current event | | `ds` | The change in scale since the previous event | | `box` | A box enclosing all touch points | In gesture events, page and client coordinates are the averages of touch coordinates and velocity is calculated from these averages. ## Drop Events ```javascript interact(dropTarget) .dropzone({ ondrop: function (event) { alert(event.relatedTarget.id + ' was dropped into ' + event.target.id) }, }) .on('dropactivate', function (event) { event.target.classList.add('drop-activated') }) ``` Dropzones can receive the following events: `dropactivate`, `dropdeactivate`, `dragenter`, `dragleave`, `dropmove`, `drop`. The dropzone events are plain objects with the following properties: | Property | Description | | --------------- | ----------------------------------------------- | | `target` | The dropzone element | | `dropzone` | The dropzone Interactable | | `relatedTarget` | The element that's being dragged | | `draggable` | The Interactable that's being dragged | | `dragEvent` | The related drag event – `drag{start,move,end}` | | `timeStamp` | Time of the event | | `type` | The event type | ## Pointer Events ```javascript interact(target).on('hold', function (event) { console.log(event.type, event.target) }) ``` - `down` - `move` - `up` - `cancel` - `tap` - `doubletap` - `hold` I call these `pointerEvents` (with a lower case "p") because they present the events roughly as the real `PointerEvent` interface does, specifically: - `event.pointerId` provides the `TouchEvent#identifier` or `PointerEvent#pointerId` or `undefined` for MouseEvents - `event.pointerType` provides the pointer type - There are no simulated mouse events after touch events The properties of the events may vary across browsers and devices depending on which event interfaces are supported. For Example, a `down` event from a `touchstart` will not provide tilt or pressure as specified in the `PointerEvent` interface. {.notice .info} ### Configuring pointer events ```javascript interact(target).pointerEvents({ holdDuration: 1000, ignoreFrom: '[no-pointer]', allowFrom: '.handle', origin: 'self', }) ``` `pointerEvent`s are not snapped or restricted, but can be modified with the origin modifications. `tap` events have a `dt` property which is the time between the related `down` and `up` events. For `doubletap` `dt` is the time between the two previous taps. `dt` for `hold` events is the length of time that the pointer has been held down for (around 600ms). ### Fast click ```javascript // fast click interact('a[href]').on('tap', function (event) { window.location.href = event.currentTarget.href event.preventDefault() }) ``` `tap` and `doubletap` don't have the delay that `click` events have on mobile devices so it works great for fast buttons and anchor links. Also, unlike regular click events, a tap isn’t fired if the pointer is moved before being released. ================================================ FILE: docs/faq.md ================================================ --- title: FAQ --- This page contains questions and issues that are frequently raised on [Gitter chat][gitter] and [Github issues][gh-issues]. ## Start action after hold Use the `hold` option which takes the number of milliseconds that the pointer must be held down for. ```javascript interact(target) .draggable({ // start dragging after the pointer is held down for 1 second hold: 1000 }) ``` If you are having problems with default browser behaviour like scrolling, context menus, etc. have a look at the [`Interactable#preventDefault`][prevent-default] method and this [thread on Github](https://github.com/taye/interact.js/issues/138). ## Clone target draggable ```html
``` ```javascript interact('.item') .draggable({ manualStart: true }) .on('move', function (event) { var interaction = event.interaction // if the pointer was moved while being held down // and an interaction hasn't started yet if (interaction.pointerIsDown && !interaction.interacting()) { var original = event.currentTarget, // create a clone of the currentTarget element clone = event.currentTarget.cloneNode(true) // insert the clone to the page // TODO: position the clone appropriately document.body.appendChild(clone) // start a drag interaction targeting the clone interaction.start({ name: 'drag' }, event.interactable, clone) } }) ``` There's no direct API to drag a clone of the target element. However, you can use [`Interaction#start`][interaction-start] to change the target of an interaction to any element that you create. ## Remove / destroy / release ```javascript interact(target).draggable(true).resizable(true) interact.isSet(target) // true interact(target).unset() interact.isSet(target) // false interact(target).draggable() // false interact(target).resizable() // false ``` To remove an Interactable, use `interact(target).unset()`. That should remove all event listeners and make interact.js forget completely about the target. ## Changing dropzones while dragging ```javascript interact.dynamicDrop(true) ``` If you're adding or removing dropzone elements or changing their dimensions while dragging, you may need to change the [`dynamicDrop`][dynamic-drop] setting to true so that the dropzones rects are recalculated after every `dragmove`. ## Drag handle ```html
A draggable item
Handle
``` ```javascript interact('.item').draggable({ allowFrom: '.handle', }) ``` To make an element be the handle of a parent draggable, use the allowFrom setting option to allow an action to start only if the element matches a certain CSS selector or is a specific element. ## Prevent actions on child ```html
A resizable item
``` ```javascript interact('.item') .draggable({ // don't drag from textarea elments ignoreFrom: 'textarea', }); ``` Use the `ignoreFrom` option to prevent actions from starting if the pointer went down on an element matching the given selector or HTMLElement. ## Revert / restore / undo drag position There's no direct API to revert a dragged element to it's position before the drag. To do this, you must store the position at `dragstart` and change the element's style so that it returns to the start position on `dragend`. You can use CSS transitions to animate change in position. ## Dragging scrolls instead ```css .draggable, .resizable, .gesturable { -ms-touch-action: none; touch-action: none; user-select: none; } ``` To allow touch interactions without scrolling or zooming, use the [`touch-action` CSS property][touch-action]. ## Dragging between iFrames There is [limited support][iframe-pr] for using interact.js across iFrames. There are currently browser inconsistencies and other issues which have yet to be addressed. [gitter]: https://gitter.im/taye/interact.js [gh-issues]: https://github.com/taye/interact.js/issues [manual-start]: /docs#manualstart [interaction-start]: /api/Interaction.html#start [prevent-default]: /api/Interactable.html#preventDefault [dynamic-drop]: /api/module-interact.html#.dynamicDrop [touch-action]: https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action [iframe-pr]: https://github.com/taye/interact.js/pull/98 ================================================ FILE: docs/gesturable.md ================================================ --- title: Gesturable --- ```html
``` ```css .draggable { touch-action: none; user-select: none; } ``` ```js var angle = 0 interact('#rotate-area').gesturable({ onmove: function (event) { var arrow = document.getElementById('arrow') angle += event.da arrow.style.webkitTransform = arrow.style.transform = 'rotate(' + angle + 'deg)' document.getElementById('angle-info').textContent = angle.toFixed(2) + '\u00b0' }, }) ``` Gesture events are triggered when two pointers go down and are moved. In gesture events, page and client coordinates are the averages of touch coordinates and velocity is calculated from these averages. The events also have the following properties: | Gesture Event property | Description | | ---------------------- | --------------------------------------------------------------------------------- | | `distance` | The distance between the event's first two touches | | `angle` | The angle of the line made by the two touches | | `da` | The change in angle since previous event | | `scale` | The ratio of the distance of the start event to the distance of the current event | | `ds` | The change in scale since the previous event | | `box` | A box enclosing all touch points | Remember to use CSS `touch-action: none` to prevent the browser from panning when the user drags with a touch pointer, and `user-select: none` to disable text selection. {.notice .info} ================================================ FILE: docs/inertia.md ================================================ --- title: Inertia --- ```javascript interact(target) .draggable({ inertia: true }) .resizable({ inertia: { resistance: 30, minSpeed: 200, endSpeed: 100 } }) ``` Inertia allows drag and resize actions to continue after the user releases the pointer at a fast enough speed. The required launch speed, end speed and resistance can optionally be configured with the settings below. If an action ends without inertia but is snapped or restricted with the `endOnly` option, then the the coordinates are interpolated from the end coords to the snapped/restricted coords. ## Options - **`resistance`** is a number greater than zero which sets the rate at which the action slows down. Higher values slow it down more quickly. - **`endSpeed`** is the speed (pixels per second) at which the action is considered to have stopped. - **`allowResume`** is a `boolean` value which indicates whether the user should be allowed to resume an action while it is in the inertia phase. - **`smoothEndDuration`** is the duration (milliseconds) of the interpolated movement from the actual end coords to the modified coords with `endOnly`. Set the value to `0` to disable end transitions with `endOnly` snap/restrict. When inertia is resumed, the difference between the start and resume coordinates relative to the target's top left corner, isn't reflected in the next `{action}move` events. Instead, an `{action}resume` event is fired when the pointer goes back down during inertia before regular "{action}move" events are fired again. If you need the difference in coords, you should listen to this event and respond to it as you would to an `{action}move` event. ================================================ FILE: docs/installation.md ================================================ --- title: Installation --- interact.js offers two sets of free packages that you can add to your project: 1. To get started quickly, you can use the package named `interactjs` on npm. This package contains all the features of the library as an _ES5 bundled_. 2. If you'd like to keep your JS payload small, there are npm packages under the `@interactjs/` scope which let you choose which features to include. These packages are distributed as _ES6 modules_ and may need to be transpiled for older browsers. ### npm pre-bundled ```sh # install pre-bundled package with all features $ npm install --save interactjs ``` ```js // es6 import import interact from 'interactjs' ``` ```js // or if using commonjs or AMD const interact = require('interactjs') ``` To use the pre-bundled package with [npm](https://docs.npmjs.com/about-npm/), install the package as a dependency with `npm install interactjs` then import or require the package in your JavaScript files. ### CDN pre-bundled ```html ``` You can also use the [jsDelivr](https://www.jsdelivr.com/package/npm/interactjs) or [unpkg](https://unpkg.com/interactjs) CDNs by adding a ` ``` The packages above are also available on `https://cdn.interactjs.io/v[VERSION]/[UNSCOPED_NAME]`. You can import them in modern browser which support ES6 `import`s. ### Ruby on Rails [Rails 5.1+](https://rubyonrails.org/) supports the [yarn](http://yarnpkg.com/) package manager, so you can add interact.js to you app by running `yarn add interactjs`. Then require the library with: ```rb //= require interactjs/interact ``` ================================================ FILE: docs/introduction.md ================================================ --- title: Introduction --- ## What is interact.js? interact.js is a JavaScript library for drag and drop, resizing and multi-touch gestures for modern browsers. Its free and open source version comes with powerful options like inertia and modifiers for snapping and restricting. The library's aim is to **present pointer input data consistently** across different browsers and devices and provide convenient ways to **pretend that the user's pointer moved in a way that it wasn't really moved** (snapping, inertia, etc.). Note that by default **interact.js doesn't move elements for you**. Styling an element so that it moves while a drag happens has to be done from your own event listeners. This way, you’re in control of everything that happens.
🌟 If you prefer to have feedback out-of-the-box, have a look at interact.js Pro. It comes with built-in hardware accelerated feedback, list reordering, spring physics, Vue & React components and more.
## Getting Started After [installing the library](/docs/installation), the basic steps to setting up your targets and interactions are: 1. Create an `Interactable` target. 2. Configure it to enable actions and add [modifiers](/docs/modifiers), [inertia](/docs/inertia), etc. 3. Add event listeners to provide visual feedback and update your app's state. For example, here's some code for [a very simple slider input](https://codepen.io/taye/pen/GgpxNq): ```js // Step 1 const slider = interact('.slider') // target elements with the "slider" class slider // Step 2 .draggable({ // make the element fire drag events origin: 'self', // (0, 0) will be the element's top-left inertia: true, // start inertial movement if thrown modifiers: [ interact.modifiers.restrict({ restriction: 'self', // keep the drag coords within the element }), ], }) // Step 3 .on('dragmove', function (event) { // call this listener on every dragmove const sliderWidth = interact.getElementRect(event.target.parentNode).width const value = event.pageX / sliderWidth event.target.style.paddingLeft = (value * 100) + '%' event.target.setAttribute('data-value', value.toFixed(2)) }) ``` The `interact` function takes an element or a CSS selector string and returns an `Interactable` object which has various methods to configure actions and event listeners. Pointer interactions of down → move → up sequences begin drag, resize, or gesture actions. By adding event listener functions for these action, you can respond to `InteractEvent`s which provide pointer coordinates, speed, element size, etc. ## Actions interact.js supports 3 basic action types which are triggered by pointer down → move → up sequences: - [Draggable](/docs/draggable) for moving elements or drawing on a canvas. This can be combined with [dropzones](/docs/dropzone) to implement drag and drop applications. - [Resizable](/docs/resizable) for watching the size and position of an element while the pointer is used to move one or two of the element's edges. - [Gesturable](/docs/gesturable) for 2-finger gestures with angle, scale, etc. data. Pro builds on the draggable action to provide [Sortable and Swappable](/docs/sortable) feature for drag and drop rearranging of lists of elements. ================================================ FILE: docs/migrating.md ================================================ --- title: Migrating from v1.2 --- The latest versions fix several bugs, allows setting more options on a per-action basis, add configuration options to `pointerEvents` and add several new methods and options. The [changelog][changelog] lists all the major changes. ### Per-action modifiers array Modifiers are now created with `interact.modifiers[modifierName](options)` methods. The return values returned by these methods go into the `actionOptions.modifiers` array. The lets you more easily reuse modifier configurations and specify their execution order. ```js // create a restrict modifier to prevent dragging an element out of its parent const restrictToParent = interact.modifiers.restrict({ restriction: 'parent', elementRect: { left: 0, right: 0, top: 1, bottom: 1 }, }) // create a snap modifier which changes the event coordinates to the closest // corner of a grid const snap100x100 = interact.modifiers.snap({ targets: [interact.snappers.grid({ x: 100, y: 100 })], relativePoints: [{ x: 0.5, y: 0.5 }], }) interact(target) .draggable({ // apply the restrict and then the snap modifiers to drag events modifiers: [restrictToParent, snap100x100], }) .on('dragmove', event => console.log(event.pageX, event.pageY)) ``` ### Improved resize snap and restrict There are a few new snap and restrict modifiers for resize actions: [Restrictions](/docs/restriction): - pointer coordinate-based `restrict` - element rect-based restriction `restrictRect` - element size-based `restrictSize` (resize only) - and element edge-based `restrictEdges` (resize only) [Snapping](/docs/snapping): - pointer coordinate-based `snap` which is best suited to drag actions, - `snapSize` which works only on resize actions and let's you set targets for the size of the target element, - and `snapEdges` which is similar to `snapSize`, but let's you set the target positions of the edges of the target element. ```js interact(target).resize({ edges: { bottom: true, right: true }, // sizes at fixed grid points snapSize: { targets: [ interact.snappers.grid({ x: 25, y: 25, range: Infinity }), ], }, // minimum size restrictSize: { min: { width: 100, height: 50 }, }, // keep the edges inside the parent restrictEdges: { outer: 'parent', endOnly: true, }, }) ``` ### Resize `aspectRatio` modifier The resize `preserveAspectRatio` and `square` options have been replaced by an `aspectRatio` modifier which can cooperate with other modifiers. ```js interact(target).resizable({ edges: { left: true, bottom: true }, modifiers: [ interact.modifiers.aspectRatio({ // ratio may be the string 'preserve' to maintain the starting aspect ratio, // or any number to force a width/height ratio ratio: 'preserve', // To add other modifiers that respect the aspect ratio, // put them in the aspectRatio.modifiers array modifiers: [interact.modifiers.restrictSize({ max: 'parent' })], }), ], }) ``` ```js interact(target).resizable({ modifiers: [ interact.modifiers.aspectRatio({ // The equalDelta option replaces the old resize.square option equalDelta: true, }), ], }) ``` ### Removed Methods The methods in the table below were removed and replaced with action method options and modifier methods for the new modifiers array API: | Method | Replaced with | | ---------------------------------------------------------- | ------------------------------------------------------ | | `interactable.squareResize(bool)` | `interact.modifiers.aspectRatio({ equalDelta: true })` | | `interactable.snap({ actions: ['drag'], ...snapOptions })` | `interact.modifiers.snap(snapOptions)` | | `interactable.restrict(restrictOptions)` | `interact.modifiers.restrict(restrictOptions)` | | `interactable.inertia(true)` | `interactable.draggable({ inertia: true })` | | `interactable.accept('.can-be-dropped')` | `interactable.dropzone({ accept: '.can-be-dropped' })` | | `interact.margin(50)` | `interactable.resizable({ margin: 50 })` | ### Action end event dx/dy The `dx` and `dy` fields on `dragend`, `resizeend` and `gestureend` events were formally the difference between the start and end coordinates. Now they are always `0` (the difference between the end and the last move event). Use `event.X0` and `event.Y0` (or `event.clientX0` and `event.clientY0`) to get the starting coordinates and subtract them from the end event coordinates. ```js interact(target).draggable({ onend: function (event) { console.log(event.pageX - event.X0, event.pageY - event.Y0) }, }) ``` ### Drop events `dragend` events are now fired _before_ `drop` events. Use `dragendEvent.relatedTarget` to get the dropzone element if there will be a drop event. ### Mouse buttons By default, only the left mouse button can start actions. The `mouseButtons` action option can be used to change this. [changelog]: https://github.com/taye/interact.js/blob/master/CHANGELOG.md ================================================ FILE: docs/modifiers.md ================================================ --- title: Modifiers --- ```js // create a restrict modifier to prevent dragging an element out of its parent const restrictToParent = interact.modifiers.restrict({ restriction: 'parent', elementRect: { left: 0, right: 0, top: 1, bottom: 1 }, }) // create a snap modifier which changes the event coordinates to the closest // corner of a grid const snap100x100 = interact.modifiers.snap({ targets: [interact.snappers.grid({ x: 100, y: 100 })], relativePoints: [{ x: 0.5, y: 0.5 }], }) interact(target) .draggable({ // apply the restrict and then the snap modifiers to drag events modifiers: [restrictToParent, snap100x100], }) .on('dragmove', event => console.log(event.pageX, event.pageY)) ``` `interact`'s `modifiers` let you change the coordinates of action events. The options object passed to action methods can have a `modifiers` array which will be applied to events of that action type. **Modifiers in the array are applied sequentially** and their order may affect the final result. ```js const snapAtEnd = interact.modifiers.snap({ endOnly: true, targets: [/* ... */], }) ``` Modifiers can be set to apply only to the last move event in an interaction by setting their `endOnly` option to `true`. When an `endOnly` modifier is used with an action that has `inertia` enabled, the event coordinates will be smoothly moved from the up coords to the modified coords. interact.js comes with a vew different types of modifiers for [snapping](/docs/snapping) and [restricting](/docs/restriction) elements. ================================================ FILE: docs/reflow.md ================================================ --- title: Reflow --- The reflow method lets you trigger an action start -> move -> end sequence which runs modifiers and does drop calculations, etc. If your interactable target is a CSS selector, an interaction will be run for each matching element. If the elements have inertia, `endOnly` modifiers and `smoothEndDuration`, then the interactions may not end immediately. The reflow method returns a `Promise` which is resolved when all interactions are complete. So you can `await` or `.then()` multiple reflows ```js const interactable = interact(target).draggable({}).resizable({}) async function onWindowResize () { // start a resize action and wait for inertia to finish await interactable.reflow({ name: drag, axis: 'x' }) // start a drag action await interactable.reflow({ name: 'resize', edges: { left: true, bottom: true }, }) } window.addEventListener(onWindowResize, 'resize') ``` ================================================ FILE: docs/resizable.md ================================================ --- title: Resizable --- ```javascript interact(target) .resizable({ edges: { top : true, // Use pointer coords to check for resize. left : false, // Disable resizing from left edge. bottom: '.resize-s',// Resize if pointer target matches selector right : handleEl // Resize if pointer target is the given Element } }) ``` Resize events have `rect` and `deltaRect` properties. `rect` is updated on each `resizemove` event and the values in `deltaRect` reflect the changes. In `resizestart`, `rect` will be identical to the rect returned by `interactable.getRect(element)` and `deltaRect` will have all-zero properties. | Resize Event property | Description | | --------------------- | ------------------------------------------------- | | `edges` | The edges of the element that are being changed | | `rect` | An object with the new dimensions of the target | | `deltaRect` | The change in dimensions since the previous event | Resizable options have an `edges` property which specifies the edges of the element which can be resized from (top, left, bottom or right). ```html
``` ```js interact('.resizable').resizable({ edges: { top: true, left: true, bottom: true, right: true }, listeners: { move (event) { let { x, y } = event.target.dataset x = (parseFloat(x) || 0) + event.deltaRect.left y = (parseFloat(y) || 0) + event.deltaRect.top Object.assign(event.target.style, { width: `${event.rect.width}px`, height: `${event.rect.height}px`, transform: `translate(${x}px, ${y}px)`, }) Object.assign(event.target.dataset, { x, y }) }, }, }) ``` Remember to use CSS `touch-action: none` to prevent the browser from panning when the user drags with a touch pointer, `user-select: none` to disable text selection, and `box-sizing: border-box` if your elements have padding and borders which affect their width. {.notice .info} If you'd like an element to behave as a resize corner, let it match the selectors of two adjacent edges. Resize handle elements must be children of the resizable element. If you need the handles to be outside the target element, then you will need to use [`Interaction#start`](interaction-start). ### `invert` ```javascript interact(target).resizable({ edges: { bottom: true, right: true }, invert: 'reposition' }) ``` By default, resize actions can't make the `event.rect` smaller than `0x0`. Use the `invert` option to specify what should happen if the target would be resized to dimensions less than `0x0`. The possible values are: - `'none'` (default) will limit the resize rect to a minimum of `0x0` - `'negate'` will allow the rect to have negative width/height - `'reposition'` will keep the width/height positive by swapping the top and bottom edges and/or swapping the left and right edges ### Aspect ratio ```js interact(target).resizable({ modifiers: [ interact.modifiers.aspectRatio({ // make sure the width is always double the height ratio: 2, // also restrict the size by nesting another modifier modifiers: [ interact.modifiers.restrictSize({ max: 'parent' }), ], }), ], }) ``` interact.js comes with an `aspectRatio` modifier which can be used to force the resized rect to maintain a certain aspect ratio. The modifier has 3 options: | Prop | Type | Description | | ------------ | -------------------- | --------------------------------------------------------------------------------------- | | `ratio` | number or 'preserve' | The aspect ratio to maintain or the value 'preserve' to maintain the starting ratio | | `equalDelta` | boolean | Increase edges by the same amount instead of maintaining the same ratio | | `modifiers` | array of modifiers | Modifiers to apply to the resize which will be made to respect the aspect ratio options | To guarantee that the aspect ratio options are respected by other modifiers, those modifiers must be in the `aspectRatio.modifiers` array option, **not** in the same `resize.modifiers` array as the `aspectRatio` one. [interaction-start]: http://interactjs.io/api/#Interaction.start ================================================ FILE: docs/restriction.md ================================================ --- title: Restrict --- interact.js has 3 restriction modifiers available through the `interact.modifiers` object: - pointer coordinate-based `restrict` - element rect-based restriction `restrictRect` - element size-based `restrictSize` (resize only) - and element edge-based `restrictEdges` (resize only) ## `restrict()` ```javascript interact(target).draggable({ modifiers: [ interact.modifiers.restrict({ restriction: 'parent', endOnly: true }) ] }) ``` The `restriction` value specifies the area that the action will be confined to. The value can be: - a rect object with `top`, `left`, `bottom` and `right` or `x`, `y`, `width` and `height`, - an element whose dimensions will be used as the restriction area, - a function which takes `(x, y, element)` and returns a rect or an element - one of these strings: - `'self'` – restrict to the target element's rect - `'parent'` – restrict to the rect of the element's parentNode or - a CSS selector string – if one of the parents of the target element matches this selector, it's rect will be used as the restriction area. ### `restrictRect()` With the `restrict` variant, restricting is by default relative to the pointer coordinates so that the action coordinates, not the element's dimensions, will be kept within the restriction area. You can use the `restrictRect` variant so that the element's edges are considered while dragging. ```javascript interact(target).draggable({ modifiers: [ interact.modifiers.restrictRect({ restriction: 'parent' }) ] }) ``` If the target element is larger than the restriction, then the element will be allowed to move around the restriction. ### `elementRect` `restrictRect` is identical to `restrict`, but the `elementRect` option is set to a helpful default of `{ left: 0, right: 0, top: 1, bottom: 1 }`. The `elementRect` option specifies the area of the element to consider as its edges as scalar values from the top left edges to the bottom right. For the `left` and `right` properties, `0` means the left edge of the element and `1` means the right edge. For `top` and `bottom`, `0 means` the top edge of the element and 1 means the bottom. `{ top: 0.25, left: 0.25, bottom: 0.75, right: 0.75 }` would result in a quarter of the element being allowed to hang over the restriction edges. ## `restrictSize()` ```javascript interact(target).resizable({ modifiers: [ interact.modifiers.restrictSize({ min: { width: 100, height: 100 }, max: { width: 500, height: 500 } }) ] }) ``` `restrictSize` lets you specify the minimum and maximum dimensions that the target element must have when resizing. ## `restrictEdges()` ```javascript interact(target).resizable({ modifiers: [ interact.modifiers.restrictEdges({ inner: { left: 100, // the left edge must be <= 100 right: 200 // the right edge must be >= 200 } outer: { left: 0, // the left edge must be >= 0 right: 300 // the right edge must be <= 300 } }) ] }) ``` `restrictEdges` lets you specify `inner` and `outer` dimensions that the target element must have when resizing. You can think of `inner` as setting the minimum size of the element and `outer` as the maximum size. ================================================ FILE: docs/snapping.md ================================================ --- title: Snapping --- interact.js has 3 snap modifiers available through the `interact.modifiers` object: - pointer coordinate-based `snap` which is best suited to drag actions, - `snapSize` which works only on resize actions and let's you set targets for the size of the target element, - and `snapEdges` which is similar to `snapSize`, but let's you set the target positions of the edges of the target element. When creating snap modifiers the options have an array of `targets`. The action events will be snapped to the closest target of this array which is within range. ## `snap()` The `snap` modifier changes the pointer coordinates to specified targets when they are within range. ```js const mySnap = interact.modifiers.snap({ targets: [ { x: 200, y: 200 }, { x: 250, y: 350 }, ], }) ``` Using the `snap` modifier while dragging, The coordinates of the pointer that the drag event listeners receive will be modified to meet the coordinates of the snap targets. This option may also be used with resizable targets, but may not yield intuitive results. `snap` targets have `x` and `y` number props and an optional `range` number property. ### `relativePoints` ```javascript interact(element).draggable({ modifiers: [ interact.modifiers.snap({ targets: [ { x: 300, y: 300 } ], relativePoints: [ { x: 0 , y: 0 }, // snap relative to the element's top-left, { x: 0.5, y: 0.5 }, // to the center { x: 1 , y: 1 } // and to the bottom-right ] }) ] }) ``` If you want to specify for `snap` (not `snapSize` or `snapEdges`) the points on the element which snapping should be relative to, then use an array of `relativePoints`. Each item in the array should be an object with `x` and `y` properties which are scalars specifying the position on the element to which snapping should be relative. If no `relativePoints` array is specified or the array is empty then snapping is relative to the pointer coordinates (default). There are effectively `targets.length * max( relativePoints.length, 1 )` snap targets while snap calculations are done. Snap functions are called multiple times with the coordinates at each `relativePoint`. ### `offset` ```javascript interact(element1).draggable({ modifiers: [ interact.modifiers.snap({ targets: [ { x: 300, y: 300 } ], offset: { x: 20, y: 20 } }) ] }) interact(element2).resizable({ modifiers: [ interact.modifiers.snap({ targets: [ { x: 300, y: 300 } ], offset: 'startCoords' }) ] }) ``` The `offset` option lets you shift the coordinates of the targets of a `snap` modifier. The value may be: - an object with `x` and `y` properties, - `'startCoords'` which will then use the `pageX` and `pageY` at the start of the action, - `'self'` which will use the target element's top-left coordinates, - or `'parent'` which will use the top-left coordinates of the target's parent element ## `snapSize()` ```js interact(target).resizable({ edges: { top: true, left: true }, modifiers: [ interact.modifiers.snapSize({ targets: [ { width: 100 }, interact.snappers.grid({ width: 100, height: 100 }), ], }), ], }) ``` The `snapSize` modifier snaps the _dimensions_ of targets when resizing. A `snapSize` target is an object with `x` and `y` number props _or_ `width` and `height` number props as well as an optional `range`. Its targets have `x` and `y` number props _or_ `width` and `height` number props as well as an optional `range`. ## `snapEdges()` ```js interact(target).resizable({ edges: { top: true, left: true }, modifiers: [ interact.modifiers.snapEdges({ targets: [ interact.snappers.grid({ top: 100, left: 100 }), ], }), ], }) ``` The `snapEdges` modifier snaps the _edges_ of targets when resizing. Its targets have either `x` and `y` number props to snap the left/right and top/bottom edges respectively, `top`, `left`, `width` and `height` number props to snap each edge and an optional `range`. ### `targets` option The coordinates of action events are compared to the targets of the provided snap modifiers. If multiple targets are within range, the closest target is used. ```js interact.modifiers.snap({ targets: [ function ( // the x and y page coordinates, x, y, // the current interaction interaction, // the offset information with relativePoint if set { x: offsetX, y: offsetY, relativePoint, index: relativePointIndex }, // the index of this function in the options.targets array index, ) { return { x: x, y: 75 + 50 * Math.sin(x * 0.04), range: 40, } }, ], }) ``` You can use functions in the `targets` array. If a snap target is a function, then it is called and given the `x` and `y` coordinates of the event as the first two parameters and the interaction as the third parameter. The return value of the function is used as a target. If a target omits an axis or edge prop, then the corresponding axis will not be changed. For example, if a target is defined as `{ y: 100, range Infinity }` then the snapped movement will be horizontal at `(100, pointerEventPageX)`. ### Snap grids ```javascript var gridTarget = interact.snappers.grid({ // can be a pair of x and y, left and top, // right and bottom, or width, and height x: 50, y: 50, // optional range: 10, // optional offset: { x: 5, y: 10 }, // optional limits: { top: 0, left: 0, bottom: 500, height: 500 } }) interact(element).draggable({ modifiers: [ interact.modifiers.snap({ targets: [gridTarget] }) ] }) ``` You can use the `interact.snappers.grid()` method to create a target that snaps to a grid. The method takes an object describing a grid and returns a function that snaps to the corners of that grid. The properties of the grid are: - `x`, `y`: the spacing between the horizontal and vertical grid lines. - `range` (optional): the distance from the grid corners within which the pointer coords will be snapped. - `offset` (optional): an object with `x` and `y` props to offset the grid lines - `limits` (optional): an object with `top`, `left`, `bottom` and `right` props to set the bounds of the grid ### `range` ```javascript interact(element).draggable({ modifiers: [ interact.modifiers.snap({ targets: [ { x: 20, y: 450, range: 50 } { x: 10, y: 0 /* use default range below */ } ], range: 300 // for targets that don't specify a range }) ] }) ``` A range can be specified in the snap modifier options and each target may optionally have its own range. The `range` of a snap target is the distance the pointer must be from the target's coordinates for a snap to be possible. i.e. `inRange = distance <= range`. ### Event snap info ```js interact(target).draggable({ modifiers: [ interact.modifiers.snap({ targets: [(x, y) => ({ x: x + 20 })] }), ], listeners: { move (event) { console.log(event.modifiers[0].target.source) }, }, }) ``` `InteractEvent.modifiers` will be an array with info on the modifiers that have been set for the action. Snap modifiers provide an object with the closest target with the calculated offset. | Prop | Type | Description | | ----------- | ------------------------- | ----------------------------------------------------------------------------- | | `x` and `y` | number | The coords that were snapped to with origin, offset and relativePoint applied | | `source` | target object or function | The target object or function in the targets array option | | `index` | number | The index of the source in the targets array | | `range` | number | The range of the target | | `offset` | object | The offset applied to the source | ================================================ FILE: docs/tooling.md ================================================ --- title: Tooling & Optimization --- ## Feature selection ```sh # install only the features you need $ npm install --save @interactjs/interact \ @interactjs/auto-start \ @interactjs/actions \ @interactjs/modifiers \ @interactjs/dev-tools ``` ```js import '@interactjs/auto-start' import '@interactjs/actions/drag' import '@interactjs/actions/resize' import '@interactjs/modifiers' import '@interactjs/dev-tools' import interact from '@interactjs/interact' interact('.item').draggable({ listeners: { move (event) { console.log(event.pageX, event.pageY) }, }, }) ``` Adding the unscoped `interactjs` npm package to your project is the easiest way to get started with the library as it includes all features already pre-bundled and compiled to ES5 syntax. However, this might result in a lot of unused features increasing the size of your JS payload. For a more streamlined build, you can add import the packages for each feature you need. See the [npm streamlined installation docs](/docs/installation#npm-streamlined) for more details including a list of available packages. ## `@interactjs/dev-tools` The `@interactjs/dev-tools` package provides hints that can help you avoid common issues (eg. missing event handlers and useful CSS styles) while developing your application. Although these hints can be helpful, it's best to avoid including them in your production deployment. There are some ways to do this below. ## Optimizing for production ### Babel plugin ```json // babel config { "env": { "production": { "plugins": [ "@interactjs/dev-tools/babel-plugin-prod", ] } } } ``` ```js // source file import '@interactjs/actions/drag' import interact from '@interactjs/interact' ``` ```js // result import '@interactjs/actions/drag/index.prod' import interact from '@interactjs/interact/index.prod' ``` If you use babel in your deployment workflow, you can simply add `@interactjs/dev-tools/babel-plugin-prod` to the plugins section of your production babel config and all `@interactjs/**` imports will be changed to the optimized, production versions with development hints optimized out. ### Without build tools ```js import '@interactjs/actions/drag/index.prod' import interact from '@interactjs/interact/index.prod' ``` If you're not using babel, then you'll need to change your imports to include the `.prod` extension. For index files of directories you'll need to add the filename (eg. `@interactjs/actions -> @interactjs/actions/index.prod`). ================================================ FILE: esnext.rollup.config.cjs ================================================ /* eslint-disable import/no-extraneous-dependencies */ const { resolve, basename, dirname, relative, extname } = require('path') const { transformAsync } = require('@babel/core') const babel = require('@rollup/plugin-babel') const commonjs = require('@rollup/plugin-commonjs') const nodeResolve = require('@rollup/plugin-node-resolve') const replace = require('@rollup/plugin-replace') const terser = require('@rollup/plugin-terser') const { glob } = require('glob') const { defineConfig } = require('rollup') const headers = require('./scripts/headers') const { getPackages, sourcesIgnoreGlobs, extendBabelOptions, getEsnextBabelOptions, getModuleDirectories, isPro, } = require('./scripts/utils') const BUNDLED_DEPS = ['rebound'] const INPUT_EXTENSIONS = ['.ts', '.tsx', '.vue'] const moduleDirectory = getModuleDirectories() module.exports = defineConfig(async () => { const packageDirs = (await getPackages()).map((dir) => resolve(__dirname, dir)) return ( await Promise.all( packageDirs.map(async (packageDir) => { const packageName = `${basename(dirname(packageDir))}/${basename(packageDir)}` const external = (id_, importer) => { const id = id_.startsWith('.') ? resolve(dirname(importer), id_) : id_ // not external if it's a dependency that's intented to be bundled if (BUNDLED_DEPS.some((dep) => id === dep || id.includes(`/node_modules/${dep}/`))) return false // not external if the id is in the current package dir if ( [packageName, packageDir].some( (prefix) => id.startsWith(prefix) && (id.length === prefix.length || id.charAt(prefix.length) === '/'), ) ) return false return true } const entryFiles = await glob('**/*.{ts,tsx}', { cwd: packageDir, ignore: sourcesIgnoreGlobs, strict: false, nodir: true, absolute: true, }) const input = Object.fromEntries( entryFiles.map((file) => [ relative(packageDir, file.slice(0, file.length - extname(file).length)), file, ]), ) return [ // dev unminified { env: { NODE_ENV: 'development' }, ext: '.js', minify: isPro }, // prod minified { env: { NODE_ENV: 'production' }, ext: '.prod.js', minify: true }, ].map(({ env, ext, minify }) => defineConfig({ external, input, plugins: [ commonjs({ include: '**/node_modules/{rebound,symbol-tree}/**' }), nodeResolve({ modulePaths: moduleDirectory, extensions: INPUT_EXTENSIONS, }), babel( extendBabelOptions( { babelrc: false, configFile: false, babelHelpers: 'bundled', skipPreflightCheck: true, extensions: INPUT_EXTENSIONS, plugins: [ [ require.resolve('@babel/plugin-transform-runtime'), { helpers: false, regenerator: false }, ], ], }, getEsnextBabelOptions(), ), ), replace({ preventAssignment: true, values: Object.entries({ npm_package_version: process.env.npm_package_version, IJS_BUNDLE: '', ...env, }).reduce((acc, [key, value]) => { acc[`process.env.${key}`] = JSON.stringify(value) return acc }, {}), }), ], context: 'window', moduleContext: 'window', preserveEntrySignatures: 'strict', output: [ { dir: packageDir, entryFileNames: `[name]${ext}`, format: 'es', banner: minify ? headers?.min : headers?.raw, inlineDynamicImports: false, sourcemap: true, plugins: [ { name: '@interactjs/_dev:output-transforms', async renderChunk(code, chunk, outputOptions) { return await transformAsync(code, { babelrc: false, configFile: false, inputSourceMap: chunk.map, filename: `${packageDir}/${chunk.fileName}`, plugins: [ [ require.resolve('./scripts/babel/relative-imports'), { extension: ext, moduleDirectory }, ], [require.resolve('@babel/plugin-transform-class-properties'), { loose: true }], ], }) }, }, minify && terser({ module: false, mangle: true, compress: { ecma: '2019', unsafe: true, unsafe_Function: true, unsafe_arrows: false, unsafe_methods: true, }, format: { preamble: headers?.min, }, }), ], }, ], }), ) }), ) ).flat() }) ================================================ FILE: examples/.eslintignore ================================================ js ================================================ FILE: examples/.eslintrc.cjs ================================================ module.exports = { extends: '../.eslintrc.cjs', globals: { interact: false, _: false, $: false }, rules: { 'no-console': 'off', 'import/no-unresolved': 'off', 'import/no-extraneous-dependencies': 'off' }, } ================================================ FILE: examples/dropzones/index.css ================================================ body { font-family: Helvetica, Arial, sans-serif; } .dropzone-wrapper { position: absolute; bottom: 0; left: 0; right: 0; } .dropzone { overflow: hidden; margin: .5em; padding: 1em; color: #666; text-align: center; background: #ccc; line-height: 4em; border: 4px dashed transparent; transition: background .15s linear, border-color .15s linear; } .dropzone.-drop-possible { border-color: #666; } .dropzone.-drop-over { background: #666; color: #fff; } .draggable { position: relative; z-index: 10; width: 200px; margin: .25em; padding: 1em 2em; background-color: #29e; color: #fff; text-align: center; -ms-touch-action: none; touch-action: none; } .draggable.-drop-possible { background-color: #42bd41; } ================================================ FILE: examples/dropzones/index.html ================================================ Highlight dropzones with interact.js
Drag me…
Drag me…
Drag me…
Drag me…
Dropzone
Dropzone
Dropzone
================================================ FILE: examples/dropzones/index.js ================================================ /* eslint-disable import/no-absolute-path */ import interact from '@interactjs/interactjs' let transformProp const dragPositions = [1, 2, 3, 4].reduce((acc, n) => { acc[`drag${n}`] = { x: 0, y: 0 } return acc }, {}) interact.maxInteractions(Infinity) // setup draggable elements. interact('.js-drag').draggable({ listeners: { start(event) { const position = dragPositions[event.target.id] position.x = parseInt(event.target.getAttribute('data-x'), 10) || 0 position.y = parseInt(event.target.getAttribute('data-y'), 10) || 0 }, move(event) { const position = dragPositions[event.target.id] position.x += event.dx position.y += event.dy if (transformProp) { event.target.style[transformProp] = 'translate(' + position.x + 'px, ' + position.y + 'px)' } else { event.target.style.left = position.x + 'px' event.target.style.top = position.y + 'px' } }, end(event) { const position = dragPositions[event.target.id] event.target.setAttribute('data-x', position.x) event.target.setAttribute('data-y', position.y) }, }, }) // setup drop areas. // dropzone #1 accepts draggable #1 setupDropzone('#drop1', '#drag1') // dropzone #2 accepts draggable #1 and #2 setupDropzone('#drop2', '#drag1, #drag2') // every dropzone accepts draggable #3 setupDropzone('.js-drop', '#drag3') /** * Setup a given element as a dropzone. * * @param {HTMLElement|String} target * @param {String} accept */ function setupDropzone(target, accept) { interact(target) .dropzone({ accept, ondropactivate: function (event) { addClass(event.relatedTarget, '-drop-possible') }, ondropdeactivate: function (event) { removeClass(event.relatedTarget, '-drop-possible') }, }) .on('dropactivate', (event) => { const active = event.target.getAttribute('active') | 0 // change style if it was previously not active if (active === 0) { addClass(event.target, '-drop-possible') event.target.textContent = 'Drop me here!' } event.target.setAttribute('active', active + 1) }) .on('dropdeactivate', (event) => { const active = event.target.getAttribute('active') | 0 // change style if it was previously active // but will no longer be active if (active === 1) { removeClass(event.target, '-drop-possible') event.target.textContent = 'Dropzone' } event.target.setAttribute('active', active - 1) }) .on('dragenter', (event) => { addClass(event.target, '-drop-over') event.relatedTarget.textContent = "I'm in" }) .on('dragleave', (event) => { removeClass(event.target, '-drop-over') event.relatedTarget.textContent = 'Drag me…' }) .on('drop', (event) => { removeClass(event.target, '-drop-over') event.relatedTarget.textContent = 'Dropped' }) } function addClass(element, className) { if (element.classList) { return element.classList.add(className) } else { element.className += ' ' + className } } function removeClass(element, className) { if (element.classList) { return element.classList.remove(className) } else { element.className = element.className.replace(new RegExp(className + ' *', 'g'), '') } } /* eslint-disable multiline-ternary */ interact(document).on('ready', () => { transformProp = 'transform' in document.body.style ? 'transform' : 'webkitTransform' in document.body.style ? 'webkitTransform' : 'mozTransform' in document.body.style ? 'mozTransform' : 'oTransform' in document.body.style ? 'oTransform' : 'msTransform' in document.body.style ? 'msTransform' : null }) /* eslint-enable multiline-ternary */ ================================================ FILE: examples/events/index.css ================================================ #swipe { position: absolute; left: 0; right: 0; top: 0; bottom: 0; margin: 0; padding: 10%; background-color: #29e; color: #fff; font-size: 8em; font-size: 10vmin; font-family: sans-serif; cursor: default; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; -o-user-select: none; user-select: none; -webkit-touch-action: none; -moz-touch-action: none; -ms-touch-action: none; touch-action: none; } .tolerance-slider { position: absolute; left: 0; bottom: 0; width: 80%; height: 5%; margin: 5% 10%; } #tolerance-display { position: absolute; left: 0; bottom: 0; width: 100%; margin: 2% 0%; font-size: 2em; font-size: 4vmin; text-align: center; color: #fff; } #tolerance-display::before { content: "interact.pointerMoveTolerance( "; } #tolerance-display::after { content: " )"; } ================================================ FILE: examples/events/index.html ================================================ interact.js events demo
Tap, drag and swipe accros the screen
1
================================================ FILE: examples/events/index.js ================================================ /* eslint-disable import/no-absolute-path */ import interact from '@interactjs/interactjs' const dirs = ['up', 'down', 'left', 'right'] interact('#swipe') .draggable(true) .on('dragend', (event) => { if (!event.swipe) { return } let str = 'swipe' for (const dir of dirs) { if (event.swipe[dir]) { str += ' ' + dir } } str += '
' + event.swipe.angle.toFixed(2) + '°' + '
' + event.swipe.speed.toFixed(2) + 'px/sec' event.target.innerHTML = str window.console.log(str.replace(/
/g, ' ')) }) const pointerEvents = ['tap', 'doubletap', 'hold', 'down', 'move', 'up'] function logEvent(event) { event.currentTarget.innerHTML = event.pointerType if (/tap|up|click|down/.test(event.type) && event.interaction.prevTap) { window.console.log( event.type + ' -- ' + event.dt + ', ' + (new Date().getTime() - event.interaction.prevTap.timeStamp), ) } if (interact.supportsTouch() || interact.supportsPointerEvent()) { event.target.innerHTML += ' #' + event.pointerId } const interactionIndex = interact.debug().interactions.list.indexOf(event.interaction) event.currentTarget.innerHTML += ' ' + event.type + '
(' + event.pageX + ', ' + event.pageY + ')
' + 'interaction #' + interactionIndex // window.console.log(event.pointerType, event.pointerId, event.type, event.pageX, event.pageY, interactionIndex); event.preventDefault() } for (let i = 0; i < pointerEvents.length; i++) { const eventType = pointerEvents[i] interact('#swipe').on(eventType, logEvent) } function changeTolerance(event) { const value = event.target.value | 0 interact.pointerMoveTolerance(value) document.getElementById('tolerance-display').textContent = value } interact('.tolerance-slider').on('input', changeTolerance) interact('.tolerance-slider').on('change', changeTolerance) ================================================ FILE: examples/iframes/bottom.html ================================================

An element in the BOTTOM frame

================================================ FILE: examples/iframes/index.css ================================================ body { margin: 0; padding: 5% 10%; font-family: sans-serif; } #drag-me { width: 25%; background-color: #29e; color: white; border: solid 0.4em #666; border-radius: 0.75em; padding: 3%; touch-action: none; position: absolute; top: 0; left: 0; } iframe { width: 100%; height: 75vh; } ================================================ FILE: examples/iframes/index.html ================================================

An element in the TOP frame

================================================ FILE: examples/iframes/index.js ================================================ export default function setInteractables() { interact('.draggable', { context: document }).draggable({ onmove: onMove, inertia: { enabled: true }, restrict: { drag: 'parent', endOnly: true, elementRect: { top: 0, left: 0, bottom: 1, right: 1 }, }, autoScroll: true, }) function onMove(event) { const target = event.target const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy if ('webkitTransform' in target.style || 'transform' in target.style) { target.style.webkitTransform = target.style.transform = 'translate(' + x + 'px, ' + y + 'px)' } else { target.style.left = x + 'px' target.style.top = y + 'px' } target.setAttribute('data-x', x) target.setAttribute('data-y', y) } } ================================================ FILE: examples/iframes/middle.html ================================================

An element in the MIDDLE frame

================================================ FILE: examples/snap/index.css ================================================ body { margin: 0; padding: 0; border: 0; font-family: "Arial", sans-serif; } canvas { position: absolute; top: 0; left: 0; margin: 20px; padding: 0; touch-action: none; } #status { width: 20%; height: 100%; position: fixed; right: 0; top: 0; padding: 5px 5px; border: none; border-left: solid 8px #3a6bff; background-color: rgba(0, 143, 179, 0.298); text-align: center; font-size: 1.4em; } #status h3 { font-size: 1.1em; margin: 3px 0px 0px 0px; padding: 0; line-height: 22px; text-transform: capitalize; font-weight: normal } #status input[type=radio], #status input[type=checkbox] { float: right; } #status [type=range] { margin: auto; width: 90%; } #status [disabled] { cursor: default; } #status label { float: left; cursor: pointer; width: 95%; text-align: left; } #modes,#sliders { overflow: hidden; width: 100%; margin: auto; } #modes.disabled label.snap-mode { cursor: default; color: gray; } ================================================ FILE: examples/snap/index.html ================================================ interact.js drag snapping Your browser does not support the HTML5 canvas

grid spacing


grid offset


snap range


snap mode







================================================ FILE: examples/snap/index.js ================================================ /* eslint-disable import/no-absolute-path */ import interact from '@interactjs/interactjs' window.interact = interact let canvas let context let guidesCanvas let guidesContext const width = 800 const height = 800 let status const blue = '#2299ee' const lightBlue = '#88ccff' const tango = '#ff4400' let draggingAnchor = null const snapOffset = { x: 0, y: 0 } const snapGrid = { x: 10, y: 10, range: 10, offset: { x: 0, y: 0 }, } const gridFunc = interact.snappers.grid(snapGrid) const anchors = [ { x: 100, y: 100, range: 200 }, { x: 600, y: 400, range: Infinity }, { x: 500, y: 150, range: Infinity }, { x: 250, y: 250, range: Infinity }, ] const prevCoords = { x: 0, y: 0 } let prevClosest = { target: { x: 0, y: 0 }, range: 0 } const cursorRadius = 10 function drawGrid(grid, gridOffset, range) { if (!grid.x || !grid.y) return const barLength = 16 const offset = { x: gridOffset.x + snapOffset.x, y: gridOffset.y + snapOffset.y, } guidesContext.clearRect(0, 0, width, height) guidesContext.fillStyle = lightBlue if (range < 0 || range === Infinity) { guidesContext.fillRect(0, 0, width, height) } for (let i = -((1 + offset.x / grid.x) | 0), lenX = width / grid.x + 1; i < lenX; i++) { for (let j = -((1 + offset.y / grid.y) | 0), lenY = height / grid.y + 1; j < lenY; j++) { if (range > 0 && range !== Infinity) { guidesContext.circle(i * grid.x + offset.x, j * grid.y + offset.y, range, blue).fill() } guidesContext.beginPath() guidesContext.moveTo(i * grid.x + offset.x, j * grid.y + offset.y - barLength / 2) guidesContext.lineTo(i * grid.x + offset.x, j * grid.y + offset.y + barLength / 2) guidesContext.stroke() guidesContext.beginPath() guidesContext.moveTo(i * grid.x + offset.x - barLength / 2, j * grid.y + offset.y) guidesContext.lineTo(i * grid.x + offset.x + barLength / 2, j * grid.y + offset.y) guidesContext.stroke() } } } function drawAnchors(defaultRange) { const barLength = 16 guidesContext.clearRect(0, 0, width, height) if (status.range.value < 0 && status.range.value !== Infinity) { guidesContext.fillStyle = lightBlue guidesContext.fillRect(0, 0, width, height) } for (let i = 0, len = anchors.length; i < len; i++) { const anchor = { x: anchors[i].x + snapOffset.x, y: anchors[i].y + snapOffset.y, range: anchors[i].range, } const range = typeof anchor.range === 'number' ? anchor.range : defaultRange if (range > 0 && range !== Infinity) { guidesContext.circle(anchor.x, anchor.y, range, blue).fill() } guidesContext.beginPath() guidesContext.moveTo(anchor.x, anchor.y - barLength / 2) guidesContext.lineTo(anchor.x, anchor.y + barLength / 2) guidesContext.stroke() guidesContext.beginPath() guidesContext.moveTo(anchor.x - barLength / 2, anchor.y) guidesContext.lineTo(anchor.x + barLength / 2, anchor.y) guidesContext.stroke() } } function drawSnap(snap) { context.clearRect(0, 0, width, height) guidesContext.clearRect(0, 0, width, height) if (status.gridMode.checked) { drawGrid(snapGrid, snapGrid.offset, snapGrid.range) } else if (status.anchorMode.checked) { drawAnchors(snap.range) } } function circle(x, y, radius, color) { this.fillStyle = color || this.fillStyle this.beginPath() this.arc(x, y, radius, 0, 2 * Math.PI) return this } window.CanvasRenderingContext2D.prototype.circle = circle function dragMove(event) { const snap = event._interaction.modification.states.find((m) => m.name === 'snap') const closest = snap && snap.closest const rect = interact.getElementRect(canvas) context.clearRect( prevCoords.x - cursorRadius - 2, prevCoords.y - cursorRadius - 2, cursorRadius * 2 + 4, cursorRadius * 2 + 4, ) context.clearRect( prevClosest.target.x - prevClosest.range - rect.left - 2, prevClosest.target.y - prevClosest.range - rect.top - 2, prevClosest.range * 2 + 4 + rect.left, prevClosest.range * 2 + 4 + rect.top, ) if (closest && closest.range !== Infinity) { const closestTarget = { x: closest.target.x - rect.left, y: closest.target.y - rect.top, } context.circle(closestTarget.x, closestTarget.y, closest.range + 1, 'rgba(102, 225, 117, 0.8)').fill() } context.circle(event.pageX, event.pageY, cursorRadius, tango).fill() prevCoords.x = event.pageX prevCoords.y = event.pageY prevClosest = closest || prevClosest } function dragEnd(event) { context.clearRect(0, 0, width, height) context.circle(event.pageX, event.pageY, cursorRadius, tango).fill() prevCoords.x = event.pageX prevCoords.y = event.pageY } function anchorDragStart(event) { if (event.snap.locked) { interact(canvas).snap(false) draggingAnchor = event.snap.anchors.closest } } function anchorDragMove(event) { if (draggingAnchor) { const snap = interact(canvas).snap().drag draggingAnchor.x += event.dx draggingAnchor.y += event.dy drawAnchors(snap.range) } } function anchorDragEnd(event) { interact(canvas).draggable(true) draggingAnchor = null } function sliderChange() { snapGrid.x = Number(status.gridX.value) snapGrid.y = Number(status.gridY.value) snapGrid.range = Number(status.range.value) snapGrid.offset.x = Number(status.offsetX.value) snapGrid.offset.y = Number(status.offsetY.value) if (snapGrid.range < 0) { snapGrid.range = Infinity } drawSnap(interact(canvas).draggable().snap) } function modeChange(event) { if (status.anchorDrag.checked && !status.anchorMode.checked) { status.anchorMode.checked = true } if (status.anchorDrag.checked) { status.anchorMode.disabled = status.offMode.disabled = status.gridMode.disabled = true status.modes.className += ' disabled' interact(canvas) .off('dragstart', dragMove) .off('dragmove', dragMove) .off('dragend', dragEnd) .on('dragstart', anchorDragStart) .on('dragmove', anchorDragMove) .on('dragend', anchorDragEnd) } else { status.anchorMode.disabled = status.offMode.disabled = status.gridMode.disabled = false status.modes.className = status.modes.className.replace(/ *\bdisabled\b/g, '') interact(canvas) .on('dragstart', dragMove) .on('dragmove', dragMove) .on('dragend', dragEnd) .off('dragstart', anchorDragStart) .off('dragmove', anchorDragMove) .off('dragend', anchorDragEnd) } interact(canvas).draggable({ inertia: { enabled: status.inertia.checked, }, modifiers: [ interact.modifiers.restrict({ restriction: 'self' }), interact.modifiers.snap({ targets: status.gridMode.checked ? [gridFunc] : status.anchorMode.checked ? anchors : null, enabled: !status.offMode.checked, endOnly: status.endOnly.checked, offset: status.relative.checked ? 'startCoords' : null, }), interact.modifiers.spring(), ], }) if (!status.relative.checked) { snapOffset.x = snapOffset.y = 0 } drawSnap(interact(canvas).draggable().snap) } function sliderInput(event) { // eslint-disable-next-line no-mixed-operators if ( (event.target.type === 'range' && // eslint-disable-next-line no-mixed-operators Number(event.target.value) > Number(event.target.max)) || Number(event.target.value) < Number(event.target.min) ) { return } sliderChange() } interact(document).on('DOMContentLoaded', () => { canvas = document.getElementById('drag') canvas.width = width canvas.height = height context = canvas.getContext('2d') interact(canvas) .on('move down', (event) => { if ((event.type === 'down' || !event.interaction.pointerIsDown) && status.relative.checked) { const rect = interact.getElementRect(canvas) snapOffset.x = event.pageX - rect.left snapOffset.y = event.pageY - rect.top drawSnap(interact(canvas).draggable().snap) } }) .draggable({ origin: 'self' }) guidesCanvas = document.getElementById('grid') guidesCanvas.width = width guidesCanvas.height = height guidesContext = guidesCanvas.getContext('2d') status = { container: document.getElementById('status'), sliders: document.getElementById('sliders'), gridX: document.getElementById('grid-x'), gridY: document.getElementById('grid-y'), offsetX: document.getElementById('offset-x'), offsetY: document.getElementById('offset-y'), range: document.getElementById('snap-range'), modes: document.getElementById('modes'), offMode: document.getElementById('off-mode'), gridMode: document.getElementById('grid-mode'), anchorMode: document.getElementById('anchor-mode'), anchorDrag: document.getElementById('drag-anchors'), endOnly: document.getElementById('end-only'), inertia: document.getElementById('inertia'), relative: document.getElementById('relative'), } interact('#sliders').on('change', sliderInput).on('input', sliderInput) interact('#modes').on('change', modeChange) sliderChange() modeChange() }) window.grid = { drawGrid, } ================================================ FILE: examples/sortable/index.html ================================================ interact.js Sortable and Swappable demo

{{ list.title }}

{{ list.items }}
{{ index }}. {{ item }}
[ {{ tags.join(', ') }} ]
{{ tag }}
================================================ FILE: examples/sortable/react.js ================================================ import '@interactjs/react' import { createElement as h, useState } from 'react' import { createRoot } from 'react-dom/client' import interact from '@interactjs/interactjs' import { getData } from './shared.js' // eslint-disable-next-line no-undef const data = getData() const { Interactable, Sortable } = interact.react.components const root = createRoot(document.getElementById('react-app')) root.render( h(() => { return h( 'div', {}, data.lists.map((list, index) => { const [items, setItems] = useState(list.items) return h('div', { key: list.title, className: 'box' }, [ list.title, h('pre', {}, JSON.stringify(items, null, 2)), h( Sortable, { className: 'container', key: `list-${list.title}`, items, onUpdate: setItems }, items.map((item) => h( Interactable, { key: item, onTap: (event) => console.log(event), className: 'item card' }, h('div', { className: 'card-content' }, item), ), ), ), ]) }), ) }), ) ================================================ FILE: examples/sortable/shared.js ================================================ import interact from '@interactjs/interactjs' function sortListener(event) { console.log(event.type, event.position) } export function getData() { return { lists: [ { title: 'Animals', items: ['elephant', 'turtle', 'frog'], }, { title: 'Numbers', items: ['first', 'second', 'third'], }, ], tags: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], } } export const sortableOptions = { draggable: { // lockAxis: 'y', inertia: true, autoScroll: { container: '.container' }, listeners: [interact.feedback.pointers()], modifiers: [ interact.modifiers.restrict({ restriction: 'html', elementRect: { left: 0, top: 0, right: 1, bottom: 1 }, }), interact.modifiers.transform(), interact.modifiers.spring(), ], }, _spillTo: document.getElementById('no-parent'), mirror: false, listeners: [ { start: sortListener, change: sortListener, end: sortListener, }, ], } export const swappableOptions = { draggable: { // lockAxis: 'y', inertia: true, modifiers: [interact.modifiers.spring()], listeners: interact.feedback.pointers(), }, } ================================================ FILE: examples/sortable/style.css ================================================ @import "bulma/css/bulma.css"; body { margin: 1em; _font-size: 2em; } .container { max-width: 500px; } .item.i-sorting { z-index: 1; } *, *::before, *::after { box-sizing: border-box; } body { _margin: 0; } .box { display: inline-flex; flex-direction: column; margin: 1em; } .container { margin: 0; transform-origin: 0 0; _transform: rotate(0deg) scale(2); _transform: rotate(10deg); width: 50vw; padding: 1em 0; flex-direction: row-reverse; } .item { margin: 1em 0; background-color: #29e; max-width: none; } .item:first-child { width: 75%; } .item.disabled { background-color: gray; } .container, .tags { touch-action: none; user-select: none; } .tags { background-color: lightgray; padding: 1em; } .tag { font-size: 2em !important; } #no-parent { position: absolute; bottom: 2em; right: 2em; width: 300px; } .i-ghost { opacity: 0.5; } .i-mirror { z-index: 100; } .container { height: 300px; overflow: auto; } ================================================ FILE: examples/sortable/vue.js ================================================ import '@interactjs/vue' import { createApp } from 'vue/dist/vue.esm-bundler' import interact from '@interactjs/interactjs' import { getData, sortableOptions, swappableOptions } from './shared.js' const app = createApp({ data() { return { ...getData(), sortableOptions, swappableOptions, } }, }) app.use(interact.vue) app.mount('#vue-app') ================================================ FILE: examples/star/index.css ================================================ .point-handle { cursor: -webkit-grab; cursor: -moz-grab; cursor: -ms-grab; cursor: grab; touch-action: none; } .dragging, .dragging .point-handle { cursor: -webkit-grabbing; cursor: -moz-grabbing; cursor: -ms-grabbing; cursor: grabbing; } ================================================ FILE: examples/star/index.js ================================================ document.addEventListener('DOMContentLoaded', () => { const sns = 'http://www.w3.org/2000/svg' const xns = 'http://www.w3.org/1999/xlink' const root = document.getElementById('svg-edit-demo') const star = document.getElementById('edit-star') let rootMatrix const originalPoints = [] let transformedPoints = [] for (let i = 0, len = star.points.numberOfItems; i < len; i++) { const handle = document.createElementNS(sns, 'use') const point = star.points.getItem(i) const newPoint = root.createSVGPoint() handle.setAttributeNS(xns, 'href', '#point-handle') handle.setAttribute('class', 'point-handle') handle.x.baseVal.value = newPoint.x = point.x handle.y.baseVal.value = newPoint.y = point.y handle.setAttribute('data-index', i) originalPoints.push(newPoint) root.appendChild(handle) } function applyTransforms(event) { rootMatrix = root.getScreenCTM() transformedPoints = originalPoints.map((point) => { return point.matrixTransform(rootMatrix) }) interact('.point-handle').draggable({ snap: { targets: transformedPoints, range: 20 * Math.max(rootMatrix.a, rootMatrix.d), }, }) } interact(root).on('mousedown', applyTransforms).on('touchstart', applyTransforms) interact('.point-handle') .draggable({ onstart: function (event) { root.setAttribute('class', 'dragging') }, onmove: function (event) { const i = event.target.getAttribute('data-index') | 0 const point = star.points.getItem(i) point.x += event.dx / rootMatrix.a point.y += event.dy / rootMatrix.d event.target.x.baseVal.value = point.x event.target.y.baseVal.value = point.y }, onend: function (event) { root.setAttribute('class', '') }, snap: { targets: originalPoints, range: 10, relativePoints: [{ x: 0.5, y: 0.5 }], }, restrict: { restriction: document.rootElement }, }) .styleCursor(false) document.addEventListener('dragstart', (event) => { event.preventDefault() }) }) ================================================ FILE: examples/svg-editor/index.html ================================================ ================================================ FILE: examples/svg-editor/index.js ================================================ /* eslint-disable import/no-absolute-path */ import '@interactjs/actions' import '@interactjs/modifiers' import '@interactjs/inertia' import '@interactjs/auto-start' import '@interactjs/dev-tools' import interact from '@interactjs/interact' const svgCanvas = document.querySelector('svg') const svgNS = 'http://www.w3.org/2000/svg' const rectangles = [] class Rectangle { constructor(x, y, w, h) { this.x = x this.y = y this.w = w this.h = h this.scale = 1.0 this.stroke = 5 this.el = document.createElementNS(svgNS, 'rect') this.el.setAttribute('data-index', rectangles.length) this.el.setAttribute('class', 'edit-rectangle') rectangles.push(this) this.draw() } draw() { let x = this.x let y = this.y let w = this.w let h = this.h let cssClass = 'edit-rectangle' if (w < 0) { x += w w = Math.abs(w) cssClass += ' neg-w' } if (h < 0) { y += h h = Math.abs(h) cssClass += ' neg-h' } this.el.setAttribute('x', x + this.stroke / 2) this.el.setAttribute('y', y + this.stroke / 2) this.el.setAttribute('width', Math.max(w, 10) - this.stroke) this.el.setAttribute('height', Math.max(h, 10) - this.stroke) this.el.setAttribute('stroke-width', this.stroke) this.el.style.transform = `scale(${this.scale})` this.el.setAttribute('class', cssClass) } } interact('.edit-rectangle') // change how interact gets the // dimensions of '.edit-rectangle' elements .rectChecker((element) => { // find the Rectangle object that the element belongs to const { x, y, w, h, scale } = rectangles[element.getAttribute('data-index')] // return a suitable object for interact.js const left = x * scale const top = y * scale return { left, top, right: left + w * scale, bottom: top + h * scale, } }) .draggable({ // inertia: true, modifiers: [ interact.modifiers.restrictRect({ // restrict to a parent element that matches this CSS selector restriction: 'svg', // only restrict before ending the drag endOnly: true, }), interact.modifiers.transform(), ], onmove: function (event) { const rectangle = rectangles[event.target.getAttribute('data-index')] rectangle.x += event.dx rectangle.y += event.dy rectangle.draw() }, }) .resizable({ edges: { left: true, right: true, top: true, bottom: true }, invert: 'reposition', modifiers: [ interact.modifiers.transform(), interact.modifiers.restrictEdges({ restriction: 'svg', }), ], listeners: { move(event) { const rectangle = rectangles[event.target.getAttribute('data-index')] rectangle.x = event.rect.left rectangle.y = event.rect.top rectangle.w = event.rect.width rectangle.h = event.rect.height rectangle.draw() }, }, }) for (let i = 0; i < 5; i++) { const r = new Rectangle(50 + 100 * i, 80, 80, 80) svgCanvas.appendChild(r.el) } interact('#invert').on('input change', (event) => { interact('.edit-rectangle').resizable({ invert: event.target.value }) console.log(event.target.value) }) ================================================ FILE: examples/transform/index.html ================================================ interact.js transforms modifier demo
item 1
item 2
item 3
item 4
================================================ FILE: examples/transform/index.js ================================================ import interact from '@interactjs/interactjs' // import Vue from '../../node_modules/vue/dist/vue.esm.browser.js' interact('.item') .draggable({ origin: 'parent', // inertia: true, modifiers: [ interact.modifiers.snap({ // enabled: false, // targets: [interact.snappers.grid({ x: 20, y: 20 })], targets: [interact.snappers.elements({ targets: '.item', range: 20 })], relativePoints: [{ x: 0, y: 0 }], }), interact.modifiers.avoid({ targets: ['.item'] }), interact.modifiers.spring({ allowResume: true }), interact.modifiers.transform(), ], }) .resizable({ enabled: true, origin: 'self', edges: { left: true, right: true, top: true, bottom: true }, modifiers: [ // interact.modifiers.snapSize({ // targets: [interact.snappers.grid({ x: 100, y: 100 })], // }), // interact.modifiers.aspectRatio({ ratio: 'preserve' }), // interact.modifiers.restrictSize({ // max: { width: 350, y: 200 }, // }), interact.modifiers.spring({ allowResume: true }), // interact.modifiers.avoid({ targets: ['.item'] }), interact.modifiers.transform(), ], }) .on('resizemove', (event) => { console.log(event.rect.width / event.rect.height) }) .on('resize drag', interact.feedback.dragResize()) ================================================ FILE: jest.config.ts ================================================ import type { Config } from '@jest/types' import { sourcesGlob } from './scripts/utils' const config: Config.InitialOptions = { preset: 'vijest', coverageThreshold: { global: { statements: 59, branches: 48, functions: 58, lines: 59, }, }, // collectCoverage: true, collectCoverageFrom: [sourcesGlob], coveragePathIgnorePatterns: ['[\\\\/]_', '\\.d\\.ts$', '@interactjs[\\\\/](rebound|symbol-tree)[\\\\/]'], coverageReporters: ['json', 'text', ['lcov', { projectRoot: 'packages/@interactjs' }]], } export default config ================================================ FILE: lerna.json ================================================ { "version": "0.0.0", "packages": [ "@interactjs/*", "./interactjs" ] } ================================================ FILE: package.json ================================================ { "name": "@interactjs/_dev", "version": "1.10.27", "private": true, "directories": { "bin": "./bin" }, "scripts": { "bootstrap": "yarn install --pure-lockfile --prefer-offline --silent && sh bin/_link", "start": "_add_plugin_indexes && vite serve", "build": "yarn build:docs && yarn build:bundle && _add_plugin_indexes && yarn build:types && yarn build:esnext", "build:docs": "yarn typedoc", "build:bundle": "rollup -c bundle.rollup.config.cjs", "build:types": "_types", "build:esnext": "_add_plugin_indexes && rollup -c esnext.rollup.config.cjs", "test": "jest", "test:debug": "node --inspect node_modules/.bin/jest --no-cache --runInBand ", "tsc_lint_test": "_add_plugin_indexes && tsc -b -f && _lint && yarn test", "prepare": "bin/_link; husky install" }, "homepage": "https://interactjs.io/pro", "description": "", "devDependencies": { "@babel/core": "^7.18.2", "@babel/plugin-proposal-export-default-from": "^7.17.12", "@babel/plugin-proposal-optional-catch-binding": "^7.16.7", "@babel/plugin-proposal-optional-chaining": "^7.17.12", "@babel/plugin-transform-class-properties": "^7.23.3", "@babel/plugin-transform-runtime": "^7.18.2", "@babel/preset-env": "^7.18.2", "@babel/preset-typescript": "^7.17.12", "@babel/runtime": "^7.18.3", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-terser": "^0.4.4", "@testing-library/dom": "^9.3.3", "@testing-library/user-event": "^14.5.1", "@types/jest": "27", "@types/node": "^17.0.42", "@types/react": "^18.2.43", "@types/shelljs": "^0.8.11", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", "@vitejs/plugin-vue": "^4.5.2", "@vue/babel-plugin-jsx": "^1.1.5", "@vue/compiler-sfc": "^3.3.11", "@vue/reactivity": "^3.3.11", "@vue/runtime-dom": "^3.3.11", "@vue/test-utils": "^2.4.2", "babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-plugin-syntax-jsx": "^6.18.0", "bulma": "^0.9.4", "del": "^7.1.0", "eslint": "^8.17.0", "eslint-config-prettier": "^9.0.0", "eslint-config-standard": "^17.0.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "^27.6.0", "eslint-plugin-markdown": "^3.0.1", "eslint-plugin-n": "^16.3.1", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.30.0", "eslint-plugin-require-path-exists": "^1.1.9", "eslint-plugin-tsdoc": "^0.2.17", "eslint-plugin-vue": "^9.1.1", "fs-extra": "^11.2.0", "glob": "^10.3.10", "hash-sum": "^2.0.0", "husky": "8.0.3", "jest": "27", "lint-staged": "^15.2.0", "mkdirp": "^3.0.1", "path-browserify": "^1.0.1", "prettier": "^3.1.1", "promise-polyfill": "^8.2.3", "react": "^18.1.0", "react-dom": "^18.1.0", "rebound": "^0.1.0", "resolve": "^1.22.0", "rollup": "4.7.0", "semver": "^7.3.7", "serve-index": "^1.9.1", "shelljs": "^0.8.5", "stylelint": "^16.0.1", "stylelint-config-css-modules": "^4.2.0", "stylelint-config-html": "^1.1.0", "stylelint-config-recess-order": "^4.2.0", "stylelint-config-standard": "^35.0.0", "temp": "^0.9.4", "ts-node": "^10.9.2", "typedoc": "^0.25.4", "typedoc-plugin-markdown": "^3.17.1", "typescript": "^5.3.3", "vijest": "^0.0.2", "vite": "^5.0.7", "vue": "^3.3.11", "yargs": "^17.5.1" }, "resolutions": { "jackspeak": "2.1.1", "query-string": "7.1.3" }, "sideEffects": false, "lint-staged": { "**/*.((ts|js|cjs)?(x)|vue|md)": "bin/_lint --fix", "**/*.(yaml|yml|json|css)": "prettier --check --write", "**/*.css": [ "stylelint --fix" ] }, "license": "MIT" } ================================================ FILE: packages/.eslintrc.cjs ================================================ module.exports = { extends: '../.eslintrc.cjs', env: { browser: true, node: false }, rules: { 'no-console': 2, strict: [2, 'never'], 'no-restricted-syntax': ['error', 'Generator', 'ExperimentalRestProperty', 'ExperimentalSpreadProperty'], }, overrides: [ { files: '**/*.spec.ts', env: { browser: true, node: true }, rules: { 'no-restricted-syntax': 'off', 'no-console': 'off' }, }, ], } ================================================ FILE: packages/@interactjs/actions/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/actions/actions.spec.ts ================================================ import type { Scope } from '@interactjs/core/scope' import * as helpers from '@interactjs/core/tests/_helpers' import type { ActionName } from '@interactjs/core/types' import * as pointerUtils from '@interactjs/utils/pointerUtils' import actions from './plugin' describe('actions integration', () => { const scope: Scope = helpers.mockScope() const event = pointerUtils.coordsToEvent(pointerUtils.newCoords()) const element = scope.document.body scope.usePlugin(actions) const interactable = scope.interactables.new(element) // make a dropzone scope.interactables.new(scope.document.documentElement).dropzone({}) const interaction1 = scope.interactions.new({}) interaction1.pointerDown(event, event, element) for (const name in scope.actions.map) { test(`${name} interaction starts and stops as expected`, () => { interaction1.start({ name: name as ActionName }, interactable, element) interaction1.stop() expect(() => { interaction1.interacting() }).not.toThrow() expect(interaction1.interacting()).toBe(false) }) } const actionNames = Object.keys(scope.actions.map) for (const order of [actionNames, [...actionNames].reverse()] as ActionName[][]) { const interaction2 = scope.interactions.new({}) for (const name of order) { test(`${name} interaction starts, moves and ends as expected`, () => { expect(() => { interaction2.start({ name }, interactable, element) interaction2.pointerMove(event, event, element) interaction2.pointerUp(event, event, element, element) }).not.toThrow() expect(interaction2.interacting()).toBe(false) }) } } }) ================================================ FILE: packages/@interactjs/actions/drag/drag.spec.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { InteractEvent } from '@interactjs/core/InteractEvent' import * as helpers from '@interactjs/core/tests/_helpers' import extend from '@interactjs/utils/extend' import * as pointerUtils from '@interactjs/utils/pointerUtils' import drag from './plugin' describe('actions/drag', () => { test('drag action init', () => { const scope = helpers.mockScope() scope.usePlugin(drag) expect(scope.actions.map.drag).toBeTruthy() expect(scope.actions.methodDict.drag).toBe('draggable') expect(typeof scope.Interactable.prototype.draggable).toBe('function') }) describe('interactable.draggable method', () => { const interactable = { options: { drag: {}, }, draggable: drag.draggable, setPerAction: () => { calledSetPerAction = true }, setOnEvents: () => { calledSetOnEvents = true }, } as unknown as Interactable let calledSetPerAction = false let calledSetOnEvents = false test('args types', () => { expect(interactable.draggable()).toEqual(interactable.options.drag) interactable.draggable(true) expect(interactable.options.drag.enabled).toBe(true) interactable.draggable(false) expect(interactable.options.drag.enabled).toBe(false) interactable.draggable({}) expect(interactable.options.drag.enabled).toBe(true) expect(calledSetOnEvents).toBe(true) expect(calledSetPerAction).toBe(true) interactable.draggable({ enabled: false }) expect(interactable.options.drag.enabled).toBe(false) }) const axisSettings = { lockAxis: ['x', 'y', 'xy', 'start'], startAxis: ['x', 'y', 'xy'], } for (const axis in axisSettings) { for (const value of axisSettings[axis]) { test(`\`${axis}: ${value}\` is set correctly`, () => { interactable.draggable({ [axis]: value }) expect(interactable.options.drag[axis]).toBe(value) }) } } }) describe('drag axis', () => { const scope = helpers.mockScope() scope.usePlugin(drag) const interaction = scope.interactions.new({}) const element = {} const interactable = { options: { drag: {}, }, target: element, } as Interactable const iEvent = { page: {}, client: {}, delta: {}, type: 'dragmove' } as InteractEvent const opposites = { x: 'y', y: 'x' } const eventCoords = { page: { x: -1, y: -2 }, client: { x: -3, y: -4 }, delta: { x: -5, y: -6 }, timeStamp: 0, } const coords = helpers.newCoordsSet() resetCoords() interaction.prepared = { name: 'drag', axis: 'xy' } interaction.interactable = interactable test('xy (any direction)', () => { scope.fire('interactions:before-action-move', { interaction } as any) expect(interaction.coords.start).toEqual(coords.start) expect(interaction.coords.delta).toEqual(coords.delta) scope.fire('interactions:action-move', { iEvent, interaction } as any) expect(iEvent.page).toEqual(eventCoords.page) expect(iEvent.delta).toEqual(eventCoords.delta) }) for (const axis in opposites) { const opposite = opposites[axis] test(`${axis}-axis`, () => { resetCoords() interaction.prepared.axis = axis as any scope.fire('interactions:action-move', { iEvent, interaction } as any) expect(iEvent.delta).toEqual({ [opposite]: 0, [axis]: eventCoords.delta[axis], }) expect(iEvent.page).toEqual({ [opposite]: coords.start.page[opposite], [axis]: eventCoords.page[axis], }) expect(iEvent.page[axis]).toBe(eventCoords.page[axis]) expect(iEvent.client[opposite]).toBe(coords.start.client[opposite]) expect(iEvent.client[axis]).toBe(eventCoords.client[axis]) }) } function resetCoords() { pointerUtils.copyCoords(iEvent, eventCoords) extend(iEvent.delta, eventCoords.delta) for (const prop in coords) { pointerUtils.copyCoords(interaction.coords[prop], coords[prop]) } } }) }) ================================================ FILE: packages/@interactjs/actions/drag/plugin.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { InteractEvent } from '@interactjs/core/InteractEvent' import type { PerActionDefaults } from '@interactjs/core/options' import type { Scope, Plugin } from '@interactjs/core/scope' import type { ListenersArg, OrBoolean } from '@interactjs/core/types' import is from '@interactjs/utils/is' declare module '@interactjs/core/Interactable' { interface Interactable { draggable(options: Partial> | boolean): this draggable(): DraggableOptions /** * ```js * interact(element).draggable({ * onstart: function (event) {}, * onmove : function (event) {}, * onend : function (event) {}, * * // the axis in which the first movement must be * // for the drag sequence to start * // 'xy' by default - any direction * startAxis: 'x' || 'y' || 'xy', * * // 'xy' by default - don't restrict to one axis (move in any direction) * // 'x' or 'y' to restrict movement to either axis * // 'start' to restrict movement to the axis the drag started in * lockAxis: 'x' || 'y' || 'xy' || 'start', * * // max number of drags that can happen concurrently * // with elements of this Interactable. Infinity by default * max: Infinity, * * // max number of drags that can target the same element+Interactable * // 1 by default * maxPerElement: 2 * }) * * var isDraggable = interact('element').draggable(); // true * ``` * * Get or set whether drag actions can be performed on the target * * @param options - true/false or An object with event * listeners to be fired on drag events (object makes the Interactable * draggable) */ draggable(options?: Partial> | boolean): this | DraggableOptions } } declare module '@interactjs/core/options' { interface ActionDefaults { drag: DraggableOptions } } declare module '@interactjs/core/types' { interface ActionMap { drag?: typeof drag } } export type DragEvent = InteractEvent<'drag'> export interface DraggableOptions extends PerActionDefaults { startAxis?: 'x' | 'y' | 'xy' lockAxis?: 'x' | 'y' | 'xy' | 'start' oninertiastart?: ListenersArg onstart?: ListenersArg onmove?: ListenersArg onend?: ListenersArg } function install(scope: Scope) { const { actions, Interactable, defaults } = scope Interactable.prototype.draggable = drag.draggable actions.map.drag = drag actions.methodDict.drag = 'draggable' defaults.actions.drag = drag.defaults } function beforeMove({ interaction }) { if (interaction.prepared.name !== 'drag') return const axis = interaction.prepared.axis if (axis === 'x') { interaction.coords.cur.page.y = interaction.coords.start.page.y interaction.coords.cur.client.y = interaction.coords.start.client.y interaction.coords.velocity.client.y = 0 interaction.coords.velocity.page.y = 0 } else if (axis === 'y') { interaction.coords.cur.page.x = interaction.coords.start.page.x interaction.coords.cur.client.x = interaction.coords.start.client.x interaction.coords.velocity.client.x = 0 interaction.coords.velocity.page.x = 0 } } function move({ iEvent, interaction }) { if (interaction.prepared.name !== 'drag') return const axis = interaction.prepared.axis if (axis === 'x' || axis === 'y') { const opposite = axis === 'x' ? 'y' : 'x' iEvent.page[opposite] = interaction.coords.start.page[opposite] iEvent.client[opposite] = interaction.coords.start.client[opposite] iEvent.delta[opposite] = 0 } } const draggable: Interactable['draggable'] = function draggable( this: Interactable, options?: DraggableOptions | boolean, ): any { if (is.object(options)) { this.options.drag.enabled = options.enabled !== false this.setPerAction('drag', options) this.setOnEvents('drag', options) if (/^(xy|x|y|start)$/.test(options.lockAxis)) { this.options.drag.lockAxis = options.lockAxis } if (/^(xy|x|y)$/.test(options.startAxis)) { this.options.drag.startAxis = options.startAxis } return this } if (is.bool(options)) { this.options.drag.enabled = options return this } return this.options.drag as DraggableOptions } const drag: Plugin = { id: 'actions/drag', install, listeners: { 'interactions:before-action-move': beforeMove, 'interactions:action-resume': beforeMove, // dragmove 'interactions:action-move': move, 'auto-start:check': (arg) => { const { interaction, interactable, buttons } = arg const dragOptions = interactable.options.drag if ( !(dragOptions && dragOptions.enabled) || // check mouseButton setting if the pointer is down (interaction.pointerIsDown && /mouse|pointer/.test(interaction.pointerType) && (buttons & interactable.options.drag.mouseButtons) === 0) ) { return undefined } arg.action = { name: 'drag', axis: dragOptions.lockAxis === 'start' ? dragOptions.startAxis : dragOptions.lockAxis, } return false }, }, draggable, beforeMove, move, defaults: { startAxis: 'xy', lockAxis: 'xy', } as DraggableOptions, getCursor() { return 'move' }, filterEventType: (type: string) => type.search('drag') === 0, } export default drag ================================================ FILE: packages/@interactjs/actions/drop/DropEvent.spec.ts ================================================ import type { InteractEvent } from '@interactjs/core/InteractEvent' import extend from '@interactjs/utils/extend' import { DropEvent } from '../drop/DropEvent' const dz1: any = { target: 'dz1', fire: jest.fn(), } const dz2: any = { target: 'dz2', fire: jest.fn(), } const el1: any = Symbol('el1') const el2: any = Symbol('el2') const interactable: any = Symbol('interactable') const dragElement: any = Symbol('drag-el') describe('DropEvent', () => { describe('constructor', () => { const interaction: any = { dropState: {} } const dragEvent = Object.freeze({ interactable, _interaction: interaction, target: dragElement, timeStamp: 10, }) as InteractEvent extend(interaction.dropState, { activeDrops: [ { dropzone: dz1, element: el1 }, { dropzone: dz2, element: el2 }, ], cur: { dropzone: dz1, element: el1 }, prev: { dropzone: dz2, element: el2 }, events: {}, }) test('dropmove target, dropzone, relatedTarget props', () => { const dropmove = new DropEvent(interaction.dropState, dragEvent, 'dropmove') expect(dropmove.target).toBe(el1) expect(dropmove.dropzone).toBe(dz1) expect(dropmove.relatedTarget).toBe(dragElement) }) test('dragleave target, dropzone, relatedTarget props', () => { const dragleave = new DropEvent(interaction.dropState, dragEvent, 'dragleave') expect(dragleave.target).toBe(el2) expect(dragleave.dropzone).toBe(dz2) expect(dragleave.relatedTarget).toBe(dragElement) }) }) describe('reject', () => { const interaction: any = { dropState: {} } const dragEvent = Object.freeze({ interactable, _interaction: interaction, target: dragElement, timeStamp: 10, }) as InteractEvent test('dropactivate.reject()', () => { extend(interaction.dropState, { activeDrops: [ { dropzone: dz1, element: el1 }, { dropzone: dz2, element: el2 }, ], cur: { dropzone: null, element: null }, prev: { dropzone: null, element: null }, events: {}, }) const dropactivate = new DropEvent(interaction.dropState, dragEvent, 'dropactivate') dropactivate.dropzone = dz1 dropactivate.target = el1 dropactivate.reject() // immediate propagation stopped on reject expect(dropactivate.propagationStopped && dropactivate.immediatePropagationStopped).toBe(true) // dropdeactivate is fired on rejected dropzone expect(dz1.fire).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'dropdeactivate' })) // activeDrop of rejected dropactivate event is removed expect(interaction.dropState.activeDrops).toEqual([{ dropzone: dz2, element: el2 }]) expect(interaction.dropState.cur).toEqual({ dropzone: null, element: null }) }) test('dropmove.reject()', () => { extend(interaction.dropState, { cur: { dropzone: dz1, element: el1 }, prev: { dropzone: null, element: null }, events: {}, }) const dropmove = new DropEvent(interaction.dropState, dragEvent, 'dropmove') dropmove.reject() // dropState.cur remains the same after rejecting non activate event, expect(interaction.dropState.cur).toEqual({ dropzone: dz1, element: el1 }) expect(interaction.dropState.rejected).toBe(true) // dragleave is fired on rejected dropzone expect(dz1.fire).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'dragleave' })) }) }) test('stop[Immediate]Propagation()', () => { const dropEvent = new DropEvent({ cur: {} } as any, {} as any, 'dragmove') expect(dropEvent.propagationStopped || dropEvent.immediatePropagationStopped).toBe(false) dropEvent.stopPropagation() expect(dropEvent.propagationStopped).toBe(true) expect(dropEvent.immediatePropagationStopped).toBe(false) dropEvent.propagationStopped = false dropEvent.stopImmediatePropagation() expect(dropEvent.propagationStopped && dropEvent.immediatePropagationStopped).toBe(true) }) }) ================================================ FILE: packages/@interactjs/actions/drop/DropEvent.ts ================================================ import { BaseEvent } from '@interactjs/core/BaseEvent' import type { Interactable } from '@interactjs/core/Interactable' import type { InteractEvent } from '@interactjs/core/InteractEvent' import type { Element } from '@interactjs/core/types' import * as arr from '@interactjs/utils/arr' import type { DropState } from './plugin' export class DropEvent extends BaseEvent<'drag'> { declare target: Element dropzone: Interactable dragEvent: InteractEvent<'drag'> relatedTarget: Element draggable: Interactable propagationStopped = false immediatePropagationStopped = false /** * Class of events fired on dropzones during drags with acceptable targets. */ constructor(dropState: DropState, dragEvent: InteractEvent<'drag'>, type: string) { super(dragEvent._interaction) const { element, dropzone } = type === 'dragleave' ? dropState.prev : dropState.cur this.type = type this.target = element this.currentTarget = element this.dropzone = dropzone this.dragEvent = dragEvent this.relatedTarget = dragEvent.target this.draggable = dragEvent.interactable this.timeStamp = dragEvent.timeStamp } /** * If this is a `dropactivate` event, the dropzone element will be * deactivated. * * If this is a `dragmove` or `dragenter`, a `dragleave` will be fired on the * dropzone element and more. */ reject() { const { dropState } = this._interaction if ( this.type !== 'dropactivate' && (!this.dropzone || dropState.cur.dropzone !== this.dropzone || dropState.cur.element !== this.target) ) { return } dropState.prev.dropzone = this.dropzone dropState.prev.element = this.target dropState.rejected = true dropState.events.enter = null this.stopImmediatePropagation() if (this.type === 'dropactivate') { const activeDrops = dropState.activeDrops const index = arr.findIndex( activeDrops, ({ dropzone, element }) => dropzone === this.dropzone && element === this.target, ) dropState.activeDrops.splice(index, 1) const deactivateEvent = new DropEvent(dropState, this.dragEvent, 'dropdeactivate') deactivateEvent.dropzone = this.dropzone deactivateEvent.target = this.target this.dropzone.fire(deactivateEvent) } else { this.dropzone.fire(new DropEvent(dropState, this.dragEvent, 'dragleave')) } } preventDefault() {} stopPropagation() { this.propagationStopped = true } stopImmediatePropagation() { this.immediatePropagationStopped = this.propagationStopped = true } } ================================================ FILE: packages/@interactjs/actions/drop/drop.spec.ts ================================================ import type Interaction from '@interactjs/core/Interaction' import * as helpers from '@interactjs/core/tests/_helpers' import drop from '../drop/plugin' describe('actions/drop', () => { afterEach(() => { document.body.innerHTML = '' }) test('options', () => { const { interactable } = helpers.testEnv({ plugins: [drop] }) const funcs = Object.freeze({ drop() {}, activate() {}, deactivate() {}, dropmove() {}, dragenter() {}, dragleave() {}, }) interactable.dropzone({ listeners: [funcs], }) expect(interactable.events.types.drop[0]).toBe(funcs.drop) expect(interactable.events.types.dropactivate[0]).toBe(funcs.activate) expect(interactable.events.types.dropdeactivate[0]).toBe(funcs.deactivate) expect(interactable.events.types.dropmove[0]).toBe(funcs.dropmove) expect(interactable.events.types.dragenter[0]).toBe(funcs.dragenter) expect(interactable.events.types.dragleave[0]).toBe(funcs.dragleave) }) test('dynamicDrop', () => { const { scope, interactable, down, start, move, interaction } = helpers.testEnv({ plugins: [drop] }) interactable.draggable({}) // no error with dynamicDrop === false expect(() => { scope.interactStatic.dynamicDrop(false) down() start({ name: 'drag' }) move() interaction.end() }).not.toThrow() // no error with dynamicDrop === true expect(() => { scope.interactStatic.dynamicDrop(true) down() start({ name: 'drag' }) move() interaction.end() }).not.toThrow() }) test('start', () => { const { scope, interactable, down, start, move, interaction } = helpers.testEnv({ plugins: [drop] }) interactable.draggable({}) const dropzone = scope.interactables.new('[data-drop]').dropzone({}) const [dropEl1, dropEl2, dropEl3] = ['a', 'b', 'c'].map((id) => { const dropEl = scope.document.createElement('div') dropEl.dataset.drop = id scope.document.body.appendChild(dropEl) return dropEl }) // rejet imeediately on activate dropzone.on('dropactivate', (event) => { if (event.target === dropEl1 || event.target === dropEl2) { event.reject() } }) const onActionsDropStart = jest.fn((arg: { interaction: Interaction }) => { const activeDrops = [...arg.interaction.dropState!.activeDrops] // actions/drop:start is fired with all activeDrops expect(activeDrops.map((activeDrop) => activeDrop.element)).toEqual([dropEl3]) }) scope.addListeners({ 'actions/drop:start': onActionsDropStart }) const onDeactivate = jest.fn() dropzone.on('dropdeactivate', onDeactivate) down() start({ name: 'drag' }) move() expect(onActionsDropStart).toHaveBeenCalledTimes(1) // rejected dropzones are removed from activeDrops expect(interaction.dropState!.activeDrops.map((d) => d.element)).toEqual([dropEl3]) // rejected dropzones are deactivated expect(onDeactivate.mock.calls.map((arg) => arg[0].target)).toEqual([dropEl1, dropEl2]) interaction.end() }) test('targeting', () => { const interactionTarget = document.body.appendChild(document.createElement('div')) interactionTarget.id = 'target' const { scope, interactable, down, start, move, up, coords } = helpers.testEnv({ plugins: [drop], target: interactionTarget, }) interactable.draggable({}) const [dropElA, dropElB, dropElC] = ['a', 'b', 'c'].map((id) => { const dropEl = scope.document.createElement('div') dropEl.dataset.drop = id scope.document.body.appendChild(dropEl) return dropEl }) const onActivate = jest.fn((event) => event.target) const onDeactivate = jest.fn((event) => event.target) const onDragenter = jest.fn() const dropzone = scope.interactables .new('[data-drop]') .dropzone({ checker: () => true, }) .on({ dropactivate: onActivate, dropdeactivate: onDeactivate, dragenter: onDragenter }) down() start({ name: 'drag' }) expect(onActivate.mock.results.map(({ value }) => value)).toEqual([dropElA, dropElB, dropElC]) expect(onDeactivate.mock.calls).toEqual([]) expect(onDragenter.mock.calls).toEqual([]) coords.page.x++ move() expect(onDragenter.mock.calls.map(([{ target, relatedTarget }]) => [target, relatedTarget])).toEqual([ [dropElC, interactionTarget], ]) onDragenter.mockClear() // only b drop dropzone.dropzone({ checker: (_dragEvent, _event, _dropped, _dropzone, dropElement) => dropElement.dataset.drop === 'b', }) coords.page.x++ move() expect(onDragenter.mock.calls.map((args) => [args[0].target, args[0].relatedTarget])).toEqual([ [dropElB, interactionTarget], ]) up() // all dropzones are deactivated expect(onDeactivate.mock.results.map(({ value }) => value)).toEqual([dropElA, dropElB, dropElC]) }) }) ================================================ FILE: packages/@interactjs/actions/drop/plugin.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { EventPhase, InteractEvent } from '@interactjs/core/InteractEvent' import type { Interaction, DoPhaseArg } from '@interactjs/core/Interaction' import type { PerActionDefaults } from '@interactjs/core/options' import type { Scope, Plugin } from '@interactjs/core/scope' import type { Element, PointerEventType, Rect, ListenersArg } from '@interactjs/core/types' import * as domUtils from '@interactjs/utils/domUtils' import extend from '@interactjs/utils/extend' import getOriginXY from '@interactjs/utils/getOriginXY' import is from '@interactjs/utils/is' import normalizeListeners from '@interactjs/utils/normalizeListeners' import * as pointerUtils from '@interactjs/utils/pointerUtils' /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import '../drag/plugin' import type { DragEvent } from '../drag/plugin' import drag from '../drag/plugin' /* eslint-enable import/no-duplicates */ import { DropEvent } from './DropEvent' export type DropFunctionChecker = ( dragEvent: any, // related drag operation event: any, // touch or mouse EventEmitter dropped: boolean, // default checker result dropzone: Interactable, // dropzone interactable dropElement: Element, // drop zone element draggable: Interactable, // draggable's Interactable draggableElement: Element, // dragged element ) => boolean export interface DropzoneOptions extends PerActionDefaults { accept?: | string | Element | (({ dropzone, draggableElement }: { dropzone: Interactable; draggableElement: Element }) => boolean) // How the overlap is checked on the drop zone overlap?: 'pointer' | 'center' | number checker?: DropFunctionChecker ondropactivate?: ListenersArg ondropdeactivate?: ListenersArg ondragenter?: ListenersArg ondragleave?: ListenersArg ondropmove?: ListenersArg ondrop?: ListenersArg } export interface DropzoneMethod { (this: Interactable, options: DropzoneOptions | boolean): Interactable (): DropzoneOptions } declare module '@interactjs/core/Interactable' { interface Interactable { /** * * ```js * interact('.drop').dropzone({ * accept: '.can-drop' || document.getElementById('single-drop'), * overlap: 'pointer' || 'center' || zeroToOne * } * ``` * * Returns or sets whether draggables can be dropped onto this target to * trigger drop events * * Dropzones can receive the following events: * - `dropactivate` and `dropdeactivate` when an acceptable drag starts and ends * - `dragenter` and `dragleave` when a draggable enters and leaves the dropzone * - `dragmove` when a draggable that has entered the dropzone is moved * - `drop` when a draggable is dropped into this dropzone * * Use the `accept` option to allow only elements that match the given CSS * selector or element. The value can be: * * - **an Element** - only that element can be dropped into this dropzone. * - **a string**, - the element being dragged must match it as a CSS selector. * - **`null`** - accept options is cleared - it accepts any element. * * Use the `overlap` option to set how drops are checked for. The allowed * values are: * * - `'pointer'`, the pointer must be over the dropzone (default) * - `'center'`, the draggable element's center must be over the dropzone * - a number from 0-1 which is the `(intersection area) / (draggable area)`. * e.g. `0.5` for drop to happen when half of the area of the draggable is * over the dropzone * * Use the `checker` option to specify a function to check if a dragged element * is over this Interactable. * * @param options - The new options to be set */ dropzone(options: DropzoneOptions | boolean): Interactable /** @returns The current setting */ dropzone(): DropzoneOptions /** * ```js * interact(target) * .dropChecker(function(dragEvent, // related dragmove or dragend event * event, // TouchEvent/PointerEvent/MouseEvent * dropped, // bool result of the default checker * dropzone, // dropzone Interactable * dropElement, // dropzone elemnt * draggable, // draggable Interactable * draggableElement) {// draggable element * * return dropped && event.target.hasAttribute('allow-drop') * } * ``` */ dropCheck( dragEvent: InteractEvent, event: PointerEventType, draggable: Interactable, draggableElement: Element, dropElemen: Element, rect: any, ): boolean } } declare module '@interactjs/core/Interaction' { interface Interaction { dropState?: DropState } } declare module '@interactjs/core/InteractEvent' { interface InteractEvent { /** @internal */ prevDropzone?: Interactable dropzone?: Interactable dragEnter?: Element dragLeave?: Element } } declare module '@interactjs/core/options' { interface ActionDefaults { drop: DropzoneOptions } } declare module '@interactjs/core/scope' { interface Scope { dynamicDrop?: boolean } interface SignalArgs { 'actions/drop:start': DropSignalArg 'actions/drop:move': DropSignalArg 'actions/drop:end': DropSignalArg } } declare module '@interactjs/core/types' { interface ActionMap { drop?: typeof drop } } declare module '@interactjs/core/InteractStatic' { interface InteractStatic { /** * Returns or sets whether the dimensions of dropzone elements are calculated * on every dragmove or only on dragstart for the default dropChecker * * @param {boolean} [newValue] True to check on each move. False to check only * before start * @return {boolean | interact} The current setting or interact */ dynamicDrop: (newValue?: boolean) => boolean | this } } interface DropSignalArg { interaction: Interaction<'drag'> dragEvent: DragEvent } export interface ActiveDrop { dropzone: Interactable element: Element rect: Rect } export interface DropState { cur: { // the dropzone a drag target might be dropped into dropzone: Interactable // the element at the time of checking element: Element } prev: { // the dropzone that was recently dragged away from dropzone: Interactable // the element at the time of checking element: Element } // wheather the potential drop was rejected from a listener rejected: boolean // the drop events related to the current drag event events: FiredDropEvents activeDrops: ActiveDrop[] } function install(scope: Scope) { const { actions, interactStatic: interact, Interactable, defaults } = scope scope.usePlugin(drag) Interactable.prototype.dropzone = function (this: Interactable, options) { return dropzoneMethod(this, options) } as Interactable['dropzone'] Interactable.prototype.dropCheck = function ( this: Interactable, dragEvent, event, draggable, draggableElement, dropElement, rect, ) { return dropCheckMethod(this, dragEvent, event, draggable, draggableElement, dropElement, rect) } interact.dynamicDrop = function (newValue?: boolean) { if (is.bool(newValue)) { // if (dragging && scope.dynamicDrop !== newValue && !newValue) { // calcRects(dropzones) // } scope.dynamicDrop = newValue return interact } return scope.dynamicDrop! } extend(actions.phaselessTypes, { dragenter: true, dragleave: true, dropactivate: true, dropdeactivate: true, dropmove: true, drop: true, }) actions.methodDict.drop = 'dropzone' scope.dynamicDrop = false defaults.actions.drop = drop.defaults } function collectDropzones({ interactables }: Scope, draggableElement: Element) { const drops: ActiveDrop[] = [] // collect all dropzones and their elements which qualify for a drop for (const dropzone of interactables.list) { if (!dropzone.options.drop.enabled) { continue } const accept = dropzone.options.drop.accept // test the draggable draggableElement against the dropzone's accept setting if ( (is.element(accept) && accept !== draggableElement) || (is.string(accept) && !domUtils.matchesSelector(draggableElement, accept)) || (is.func(accept) && !accept({ dropzone, draggableElement })) ) { continue } for (const dropzoneElement of dropzone.getAllElements()) { if (dropzoneElement !== draggableElement) { drops.push({ dropzone, element: dropzoneElement, rect: dropzone.getRect(dropzoneElement), }) } } } return drops } function fireActivationEvents(activeDrops: ActiveDrop[], event: DropEvent) { // loop through all active dropzones and trigger event for (const { dropzone, element } of activeDrops.slice()) { event.dropzone = dropzone // set current element as event target event.target = element dropzone.fire(event) event.propagationStopped = event.immediatePropagationStopped = false } } // return a new array of possible drops. getActiveDrops should always be // called when a drag has just started or a drag event happens while // dynamicDrop is true function getActiveDrops(scope: Scope, dragElement: Element) { // get dropzones and their elements that could receive the draggable const activeDrops = collectDropzones(scope, dragElement) for (const activeDrop of activeDrops) { activeDrop.rect = activeDrop.dropzone.getRect(activeDrop.element) } return activeDrops } function getDrop( { dropState, interactable: draggable, element: dragElement }: Interaction, dragEvent, pointerEvent, ) { const validDrops: Element[] = [] // collect all dropzones and their elements which qualify for a drop for (const { dropzone, element: dropzoneElement, rect } of dropState.activeDrops) { const isValid = dropzone.dropCheck( dragEvent, pointerEvent, draggable!, dragElement!, dropzoneElement, rect, ) validDrops.push(isValid ? dropzoneElement : null) } // get the most appropriate dropzone based on DOM depth and order const dropIndex = domUtils.indexOfDeepestElement(validDrops) return dropState!.activeDrops[dropIndex] || null } function getDropEvents(interaction: Interaction, _pointerEvent, dragEvent: DragEvent) { const dropState = interaction.dropState! const dropEvents: Record = { enter: null, leave: null, activate: null, deactivate: null, move: null, drop: null, } if (dragEvent.type === 'dragstart') { dropEvents.activate = new DropEvent(dropState, dragEvent, 'dropactivate') dropEvents.activate.target = null as never dropEvents.activate.dropzone = null as never } if (dragEvent.type === 'dragend') { dropEvents.deactivate = new DropEvent(dropState, dragEvent, 'dropdeactivate') dropEvents.deactivate.target = null as never dropEvents.deactivate.dropzone = null as never } if (dropState.rejected) { return dropEvents } if (dropState.cur.element !== dropState.prev.element) { // if there was a previous dropzone, create a dragleave event if (dropState.prev.dropzone) { dropEvents.leave = new DropEvent(dropState, dragEvent, 'dragleave') dragEvent.dragLeave = dropEvents.leave.target = dropState.prev.element dragEvent.prevDropzone = dropEvents.leave.dropzone = dropState.prev.dropzone } // if dropzone is not null, create a dragenter event if (dropState.cur.dropzone) { dropEvents.enter = new DropEvent(dropState, dragEvent, 'dragenter') dragEvent.dragEnter = dropState.cur.element dragEvent.dropzone = dropState.cur.dropzone } } if (dragEvent.type === 'dragend' && dropState.cur.dropzone) { dropEvents.drop = new DropEvent(dropState, dragEvent, 'drop') dragEvent.dropzone = dropState.cur.dropzone dragEvent.relatedTarget = dropState.cur.element } if (dragEvent.type === 'dragmove' && dropState.cur.dropzone) { dropEvents.move = new DropEvent(dropState, dragEvent, 'dropmove') dragEvent.dropzone = dropState.cur.dropzone } return dropEvents } type FiredDropEvents = Partial< Record<'leave' | 'enter' | 'move' | 'drop' | 'activate' | 'deactivate', DropEvent> > function fireDropEvents(interaction: Interaction, events: FiredDropEvents) { const dropState = interaction.dropState! const { activeDrops, cur, prev } = dropState if (events.leave) { prev.dropzone.fire(events.leave) } if (events.enter) { cur.dropzone.fire(events.enter) } if (events.move) { cur.dropzone.fire(events.move) } if (events.drop) { cur.dropzone.fire(events.drop) } if (events.deactivate) { fireActivationEvents(activeDrops, events.deactivate) } dropState.prev.dropzone = cur.dropzone dropState.prev.element = cur.element } function onEventCreated({ interaction, iEvent, event }: DoPhaseArg<'drag', EventPhase>, scope: Scope) { if (iEvent.type !== 'dragmove' && iEvent.type !== 'dragend') { return } const dropState = interaction.dropState! if (scope.dynamicDrop) { dropState.activeDrops = getActiveDrops(scope, interaction.element!) } const dragEvent = iEvent const dropResult = getDrop(interaction, dragEvent, event) // update rejected status dropState.rejected = dropState.rejected && !!dropResult && dropResult.dropzone === dropState.cur.dropzone && dropResult.element === dropState.cur.element dropState.cur.dropzone = dropResult && dropResult.dropzone dropState.cur.element = dropResult && dropResult.element dropState.events = getDropEvents(interaction, event, dragEvent) } function dropzoneMethod(interactable: Interactable): DropzoneOptions function dropzoneMethod(interactable: Interactable, options: DropzoneOptions | boolean): Interactable function dropzoneMethod(interactable: Interactable, options?: DropzoneOptions | boolean) { if (is.object(options)) { interactable.options.drop.enabled = options.enabled !== false if (options.listeners) { const normalized = normalizeListeners(options.listeners) // rename 'drop' to '' as it will be prefixed with 'drop' const corrected = Object.keys(normalized).reduce((acc, type) => { const correctedType = /^(enter|leave)/.test(type) ? `drag${type}` : /^(activate|deactivate|move)/.test(type) ? `drop${type}` : type acc[correctedType] = normalized[type] return acc }, {}) const prevListeners = interactable.options.drop.listeners prevListeners && interactable.off(prevListeners) interactable.on(corrected) interactable.options.drop.listeners = corrected } if (is.func(options.ondrop)) { interactable.on('drop', options.ondrop) } if (is.func(options.ondropactivate)) { interactable.on('dropactivate', options.ondropactivate) } if (is.func(options.ondropdeactivate)) { interactable.on('dropdeactivate', options.ondropdeactivate) } if (is.func(options.ondragenter)) { interactable.on('dragenter', options.ondragenter) } if (is.func(options.ondragleave)) { interactable.on('dragleave', options.ondragleave) } if (is.func(options.ondropmove)) { interactable.on('dropmove', options.ondropmove) } if (/^(pointer|center)$/.test(options.overlap as string)) { interactable.options.drop.overlap = options.overlap } else if (is.number(options.overlap)) { interactable.options.drop.overlap = Math.max(Math.min(1, options.overlap), 0) } if ('accept' in options) { interactable.options.drop.accept = options.accept } if ('checker' in options) { interactable.options.drop.checker = options.checker } return interactable } if (is.bool(options)) { interactable.options.drop.enabled = options return interactable } return interactable.options.drop } function dropCheckMethod( interactable: Interactable, dragEvent: InteractEvent, event: PointerEventType, draggable: Interactable, draggableElement: Element, dropElement: Element, rect: any, ) { let dropped = false // if the dropzone has no rect (eg. display: none) // call the custom dropChecker or just return false if (!(rect = rect || interactable.getRect(dropElement))) { return interactable.options.drop.checker ? interactable.options.drop.checker( dragEvent, event, dropped, interactable, dropElement, draggable, draggableElement, ) : false } const dropOverlap = interactable.options.drop.overlap if (dropOverlap === 'pointer') { const origin = getOriginXY(draggable, draggableElement, 'drag') const page = pointerUtils.getPageXY(dragEvent) page.x += origin.x page.y += origin.y const horizontal = page.x > rect.left && page.x < rect.right const vertical = page.y > rect.top && page.y < rect.bottom dropped = horizontal && vertical } const dragRect = draggable.getRect(draggableElement) if (dragRect && dropOverlap === 'center') { const cx = dragRect.left + dragRect.width / 2 const cy = dragRect.top + dragRect.height / 2 dropped = cx >= rect.left && cx <= rect.right && cy >= rect.top && cy <= rect.bottom } if (dragRect && is.number(dropOverlap)) { const overlapArea = Math.max(0, Math.min(rect.right, dragRect.right) - Math.max(rect.left, dragRect.left)) * Math.max(0, Math.min(rect.bottom, dragRect.bottom) - Math.max(rect.top, dragRect.top)) const overlapRatio = overlapArea / (dragRect.width * dragRect.height) dropped = overlapRatio >= dropOverlap } if (interactable.options.drop.checker) { dropped = interactable.options.drop.checker( dragEvent, event, dropped, interactable, dropElement, draggable, draggableElement, ) } return dropped } const drop: Plugin = { id: 'actions/drop', install, listeners: { 'interactions:before-action-start': ({ interaction }) => { if (interaction.prepared.name !== 'drag') { return } interaction.dropState = { cur: { dropzone: null, element: null, }, prev: { dropzone: null, element: null, }, rejected: null, events: null, activeDrops: [], } }, 'interactions:after-action-start': ( { interaction, event, iEvent: dragEvent }: DoPhaseArg<'drag', EventPhase>, scope, ) => { if (interaction.prepared.name !== 'drag') { return } const dropState = interaction.dropState! // reset active dropzones dropState.activeDrops = [] dropState.events = {} dropState.activeDrops = getActiveDrops(scope, interaction.element!) dropState.events = getDropEvents(interaction, event, dragEvent) if (dropState.events.activate) { fireActivationEvents(dropState.activeDrops, dropState.events.activate) scope.fire('actions/drop:start', { interaction, dragEvent }) } }, 'interactions:action-move': onEventCreated, 'interactions:after-action-move': ( { interaction, iEvent: dragEvent }: DoPhaseArg<'drag', EventPhase>, scope, ) => { if (interaction.prepared.name !== 'drag') { return } const dropState = interaction.dropState! fireDropEvents(interaction, dropState.events) scope.fire('actions/drop:move', { interaction, dragEvent }) dropState.events = {} }, 'interactions:action-end': (arg: DoPhaseArg<'drag', EventPhase>, scope) => { if (arg.interaction.prepared.name !== 'drag') { return } const { interaction, iEvent: dragEvent } = arg onEventCreated(arg, scope) fireDropEvents(interaction, interaction.dropState!.events) scope.fire('actions/drop:end', { interaction, dragEvent }) }, 'interactions:stop': ({ interaction }) => { if (interaction.prepared.name !== 'drag') { return } const { dropState } = interaction if (dropState) { dropState.activeDrops = null as never dropState.events = null as never dropState.cur.dropzone = null as never dropState.cur.element = null as never dropState.prev.dropzone = null as never dropState.prev.element = null as never dropState.rejected = false } }, }, getActiveDrops, getDrop, getDropEvents, fireDropEvents, filterEventType: (type: string) => type.search('drag') === 0 || type.search('drop') === 0, defaults: { enabled: false, accept: null as never, overlap: 'pointer', } as DropzoneOptions, } export default drop ================================================ FILE: packages/@interactjs/actions/gesture/gesture.spec.ts ================================================ import type { Scope } from '@interactjs/core/scope' import * as helpers from '@interactjs/core/tests/_helpers' import extend from '@interactjs/utils/extend' import { coordsToEvent, newCoords } from '@interactjs/utils/pointerUtils' import type { GestureEvent } from './plugin' import gesture from './plugin' function getGestureProps(event: GestureEvent) { return helpers.getProps(event, ['type', 'angle', 'distance', 'scale', 'ds', 'da']) } describe('actions/gesture', () => { test('action init', () => { const scope: Scope = helpers.mockScope() scope.usePlugin(gesture) expect(scope.actions.map.gesture).toBeTruthy() expect(scope.actions.methodDict.gesture).toBe('gesturable') expect(scope.Interactable.prototype.gesturable).toBeInstanceOf(Function) }) test('interactable.gesturable() method', () => { const rect = Object.freeze({ top: 100, left: 200, bottom: 300, right: 400 }) const { scope, interaction, interactable, target: element, coords, down, start, move, } = helpers.testEnv({ plugins: [gesture], rect, }) const events: GestureEvent[] = [] const event2 = coordsToEvent(newCoords()) event2.coords.pointerId = 2 scope.usePlugin(gesture) interactable.rectChecker(() => ({ ...rect })) interactable.gesturable(true) interactable.on('gesturestart gesturemove gestureend', (event: GestureEvent) => { events.push(event) }) interaction.pointerType = 'touch' // 0 ➡ 1 extend(coords.page, { x: 0, y: 0 }) extend(event2.coords.page, { x: 100, y: 0 }) const checkArg = { action: null, interactable, interaction, element, rect, buttons: 0, } down() scope.fire('auto-start:check', checkArg) // not allowed with 1 pointer expect(checkArg.action).toBeFalsy() interaction.pointerDown(event2, event2, element) scope.fire('auto-start:check', checkArg) // allowed with 2 pointers expect(checkArg.action).toBeTruthy() start({ name: 'gesture' }) // start interaction properties are correct, expect(interaction.gesture).toEqual({ angle: 0, distance: 100, scale: 1, startAngle: 0, startDistance: 100, }) // start event properties are correct, expect(getGestureProps(events[0])).toEqual({ type: 'gesturestart', angle: 0, distance: 100, scale: 1, ds: 0, da: 0, }) // 0 // ⬇ // 1 extend(event2.coords.page, { x: 0, y: 50 }) interaction.pointerMove(event2, event2, element) // move interaction properties are correct, expect(interaction.gesture).toEqual({ angle: 90, distance: 50, scale: 0.5, startAngle: 0, startDistance: 100, }) // move event properties are correct, expect(getGestureProps(events[1])).toEqual({ type: 'gesturemove', angle: 90, distance: 50, scale: 0.5, ds: -0.5, da: 90, }) // 1 ⬅ 0 extend(coords.page, { x: 50, y: 50 }) move() // move interaction properties are correct, expect(interaction.gesture).toEqual({ angle: 180, distance: 50, scale: 0.5, startAngle: 0, startDistance: 100, }) // move event properties are correct, expect(getGestureProps(events[2])).toEqual({ type: 'gesturemove', angle: 180, distance: 50, scale: 0.5, ds: 0, da: 90, }) interaction.pointerUp(event2, event2, element, element) // move interaction properties are correct, expect(interaction.gesture).toEqual({ angle: 180, distance: 50, scale: 0.5, startAngle: 0, startDistance: 100, }) // end event properties are correct, expect(getGestureProps(events[3])).toEqual({ type: 'gestureend', angle: 180, distance: 50, scale: 0.5, ds: 0, da: 0, }) // 0 // ⬇ // 1 interaction.pointerDown(event2, event2, element) extend(coords.page, { x: 0, y: -150 }) checkArg.action = null scope.fire('auto-start:check', checkArg) interaction.pointerMove(event2, event2, element) // not allowed with re-added second pointers expect(checkArg.action).toBeTruthy() interaction.start({ name: 'gesture' }, interactable, element) // move interaction properties are correct, expect(interaction.gesture).toEqual({ angle: 90, distance: 200, scale: 1, startAngle: 90, startDistance: 200, }) // second start event properties are correct, expect(getGestureProps(events[4])).toEqual({ type: 'gesturestart', angle: 90, distance: 200, scale: 1, ds: 0, da: 0, }) // correct number of events fired expect(events).toHaveLength(5) }) }) ================================================ FILE: packages/@interactjs/actions/gesture/plugin.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { InteractEvent, EventPhase } from '@interactjs/core/InteractEvent' import type { Interaction, DoPhaseArg } from '@interactjs/core/Interaction' import type { PerActionDefaults } from '@interactjs/core/options' import type { Scope, Plugin } from '@interactjs/core/scope' import type { Rect, PointerType, ListenersArg, OrBoolean } from '@interactjs/core/types' import is from '@interactjs/utils/is' import * as pointerUtils from '@interactjs/utils/pointerUtils' declare module '@interactjs/core/Interaction' { interface Interaction { gesture?: { angle: number // angle from first to second touch distance: number scale: number // gesture.distance / gesture.startDistance startAngle: number // angle of line joining two touches startDistance: number // distance between two touches of touchStart } } } declare module '@interactjs/core/Interactable' { interface Interactable { gesturable(options: Partial> | boolean): this gesturable(): GesturableOptions /** * ```js * interact(element).gesturable({ * onstart: function (event) {}, * onmove : function (event) {}, * onend : function (event) {}, * * // limit multiple gestures. * // See the explanation in {@link Interactable.draggable} example * max: Infinity, * maxPerElement: 1, * }) * * var isGestureable = interact(element).gesturable() * ``` * * Gets or sets whether multitouch gestures can be performed on the target * * @param options - true/false or An object with event listeners to be fired on gesture events (makes the Interactable gesturable) * @returns A boolean indicating if this can be the target of gesture events, or this Interactable */ gesturable(options?: Partial> | boolean): this | GesturableOptions } } declare module '@interactjs/core/options' { interface ActionDefaults { gesture: GesturableOptions } } declare module '@interactjs/core/types' { interface ActionMap { gesture?: typeof gesture } } export interface GesturableOptions extends PerActionDefaults { onstart?: ListenersArg onmove?: ListenersArg onend?: ListenersArg } export interface GestureEvent extends InteractEvent<'gesture'> { distance: number angle: number da: number // angle change scale: number // ratio of distance start to current event ds: number // scale change box: Rect // enclosing box of all points touches: PointerType[] } export interface GestureSignalArg extends DoPhaseArg<'gesture', EventPhase> { iEvent: GestureEvent interaction: Interaction<'gesture'> } function install(scope: Scope) { const { actions, Interactable, defaults } = scope Interactable.prototype.gesturable = function ( this: InstanceType, options: GesturableOptions | boolean, ) { if (is.object(options)) { this.options.gesture.enabled = options.enabled !== false this.setPerAction('gesture', options) this.setOnEvents('gesture', options) return this } if (is.bool(options)) { this.options.gesture.enabled = options return this } return this.options.gesture as GesturableOptions } as Interactable['gesturable'] actions.map.gesture = gesture actions.methodDict.gesture = 'gesturable' defaults.actions.gesture = gesture.defaults } function updateGestureProps({ interaction, iEvent, phase }: GestureSignalArg) { if (interaction.prepared.name !== 'gesture') return const pointers = interaction.pointers.map((p) => p.pointer) const starting = phase === 'start' const ending = phase === 'end' const deltaSource = interaction.interactable.options.deltaSource iEvent.touches = [pointers[0], pointers[1]] if (starting) { iEvent.distance = pointerUtils.touchDistance(pointers, deltaSource) iEvent.box = pointerUtils.touchBBox(pointers) iEvent.scale = 1 iEvent.ds = 0 iEvent.angle = pointerUtils.touchAngle(pointers, deltaSource) iEvent.da = 0 interaction.gesture.startDistance = iEvent.distance interaction.gesture.startAngle = iEvent.angle } else if (ending || interaction.pointers.length < 2) { const prevEvent = interaction.prevEvent as GestureEvent iEvent.distance = prevEvent.distance iEvent.box = prevEvent.box iEvent.scale = prevEvent.scale iEvent.ds = 0 iEvent.angle = prevEvent.angle iEvent.da = 0 } else { iEvent.distance = pointerUtils.touchDistance(pointers, deltaSource) iEvent.box = pointerUtils.touchBBox(pointers) iEvent.scale = iEvent.distance / interaction.gesture.startDistance iEvent.angle = pointerUtils.touchAngle(pointers, deltaSource) iEvent.ds = iEvent.scale - interaction.gesture.scale iEvent.da = iEvent.angle - interaction.gesture.angle } interaction.gesture.distance = iEvent.distance interaction.gesture.angle = iEvent.angle if (is.number(iEvent.scale) && iEvent.scale !== Infinity && !isNaN(iEvent.scale)) { interaction.gesture.scale = iEvent.scale } } const gesture: Plugin = { id: 'actions/gesture', before: ['actions/drag', 'actions/resize'], install, listeners: { 'interactions:action-start': updateGestureProps, 'interactions:action-move': updateGestureProps, 'interactions:action-end': updateGestureProps, 'interactions:new': ({ interaction }) => { interaction.gesture = { angle: 0, distance: 0, scale: 1, startAngle: 0, startDistance: 0, } }, 'auto-start:check': (arg) => { if (arg.interaction.pointers.length < 2) { return undefined } const gestureOptions = arg.interactable.options.gesture if (!(gestureOptions && gestureOptions.enabled)) { return undefined } arg.action = { name: 'gesture' } return false }, }, defaults: {}, getCursor() { return '' }, filterEventType: (type: string) => type.search('gesture') === 0, } export default gesture ================================================ FILE: packages/@interactjs/actions/package.json ================================================ { "name": "@interactjs/actions", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/actions" }, "peerDependencies": { "@interactjs/core": "1.10.27", "@interactjs/utils": "1.10.27" }, "optionalDependencies": { "@interactjs/interact": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/actions/plugin.ts ================================================ import type { Scope } from '@interactjs/core/scope' /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import './drag/plugin' import './drop/plugin' import './gesture/plugin' import './resize/plugin' import drag from './drag/plugin' import drop from './drop/plugin' import gesture from './gesture/plugin' import resize from './resize/plugin' /* eslint-enable import/no-duplicates */ export default { id: 'actions', install(scope: Scope) { scope.usePlugin(gesture) scope.usePlugin(resize) scope.usePlugin(drag) scope.usePlugin(drop) }, } ================================================ FILE: packages/@interactjs/actions/resize/plugin.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { EventPhase, InteractEvent } from '@interactjs/core/InteractEvent' import type { Interaction } from '@interactjs/core/Interaction' import type { PerActionDefaults } from '@interactjs/core/options' import type { Scope, Plugin } from '@interactjs/core/scope' import type { ActionName, ActionProps, EdgeOptions, FullRect, ListenersArg, OrBoolean, Point, Rect, } from '@interactjs/core/types' import * as dom from '@interactjs/utils/domUtils' import extend from '@interactjs/utils/extend' import is from '@interactjs/utils/is' export type EdgeName = 'top' | 'left' | 'bottom' | 'right' declare module '@interactjs/core/Interactable' { interface Interactable { resizable(): ResizableOptions resizable(options: Partial> | boolean): this /** * ```js * interact(element).resizable({ * onstart: function (event) {}, * onmove : function (event) {}, * onend : function (event) {}, * * edges: { * top : true, // Use pointer coords to check for resize. * left : false, // Disable resizing from left edge. * bottom: '.resize-s',// Resize if pointer target matches selector * right : handleEl // Resize if pointer target is the given Element * }, * * // Width and height can be adjusted independently. When `true`, width and * // height are adjusted at a 1:1 ratio. * square: false, * * // Width and height can be adjusted independently. When `true`, width and * // height maintain the aspect ratio they had when resizing started. * preserveAspectRatio: false, * * // a value of 'none' will limit the resize rect to a minimum of 0x0 * // 'negate' will allow the rect to have negative width/height * // 'reposition' will keep the width/height positive by swapping * // the top and bottom edges and/or swapping the left and right edges * invert: 'none' || 'negate' || 'reposition' * * // limit multiple resizes. * // See the explanation in the {@link Interactable.draggable} example * max: Infinity, * maxPerElement: 1, * }) * * var isResizeable = interact(element).resizable() * ``` * * Gets or sets whether resize actions can be performed on the target * * @param options - true/false or An object with event * listeners to be fired on resize events (object makes the Interactable * resizable) * @returns A boolean indicating if this can be the * target of resize elements, or this Interactable */ resizable(options?: Partial> | boolean): this | ResizableOptions } } declare module '@interactjs/core/Interaction' { interface Interaction { resizeAxes: 'x' | 'y' | 'xy' styleCursor(newValue: boolean): this styleCursor(): boolean resizeStartAspectRatio: number } } declare module '@interactjs/core/options' { interface ActionDefaults { resize: ResizableOptions } } declare module '@interactjs/core/types' { interface ActionMap { resize?: typeof resize } } export interface ResizableOptions extends PerActionDefaults { square?: boolean preserveAspectRatio?: boolean edges?: EdgeOptions | null axis?: 'x' | 'y' | 'xy' // deprecated invert?: 'none' | 'negate' | 'reposition' margin?: number squareResize?: boolean oninertiastart?: ListenersArg onstart?: ListenersArg onmove?: ListenersArg onend?: ListenersArg } export interface ResizeEvent

extends InteractEvent<'resize', P> { deltaRect?: FullRect edges?: ActionProps['edges'] } function install(scope: Scope) { const { actions, browser, Interactable, // tslint:disable-line no-shadowed-variable defaults, } = scope // Less Precision with touch input resize.cursors = initCursors(browser) resize.defaultMargin = browser.supportsTouch || browser.supportsPointerEvent ? 20 : 10 Interactable.prototype.resizable = function (this: Interactable, options: ResizableOptions | boolean) { return resizable(this, options, scope) } as Interactable['resizable'] actions.map.resize = resize actions.methodDict.resize = 'resizable' defaults.actions.resize = resize.defaults } function resizeChecker(arg) { const { interaction, interactable, element, rect, buttons } = arg if (!rect) { return undefined } const page = extend({}, interaction.coords.cur.page) const resizeOptions = interactable.options.resize if ( !(resizeOptions && resizeOptions.enabled) || // check mouseButton setting if the pointer is down (interaction.pointerIsDown && /mouse|pointer/.test(interaction.pointerType) && (buttons & resizeOptions.mouseButtons) === 0) ) { return undefined } // if using resize.edges if (is.object(resizeOptions.edges)) { const resizeEdges = { left: false, right: false, top: false, bottom: false, } for (const edge in resizeEdges) { resizeEdges[edge] = checkResizeEdge( edge, resizeOptions.edges[edge], page, interaction._latestPointer.eventTarget, element, rect, resizeOptions.margin || resize.defaultMargin, ) } resizeEdges.left = resizeEdges.left && !resizeEdges.right resizeEdges.top = resizeEdges.top && !resizeEdges.bottom if (resizeEdges.left || resizeEdges.right || resizeEdges.top || resizeEdges.bottom) { arg.action = { name: 'resize', edges: resizeEdges, } } } else { const right = resizeOptions.axis !== 'y' && page.x > rect.right - resize.defaultMargin const bottom = resizeOptions.axis !== 'x' && page.y > rect.bottom - resize.defaultMargin if (right || bottom) { arg.action = { name: 'resize', axes: (right ? 'x' : '') + (bottom ? 'y' : ''), } } } return arg.action ? false : undefined } function resizable(interactable: Interactable, options: OrBoolean | boolean, scope: Scope) { if (is.object(options)) { interactable.options.resize.enabled = options.enabled !== false interactable.setPerAction('resize', options) interactable.setOnEvents('resize', options) if (is.string(options.axis) && /^x$|^y$|^xy$/.test(options.axis)) { interactable.options.resize.axis = options.axis } else if (options.axis === null) { interactable.options.resize.axis = scope.defaults.actions.resize.axis } if (is.bool(options.preserveAspectRatio)) { interactable.options.resize.preserveAspectRatio = options.preserveAspectRatio } else if (is.bool(options.square)) { interactable.options.resize.square = options.square } return interactable } if (is.bool(options)) { interactable.options.resize.enabled = options return interactable } return interactable.options.resize } function checkResizeEdge( name: string, value: any, page: Point, element: Node, interactableElement: Element, rect: Rect, margin: number, ) { // false, '', undefined, null if (!value) { return false } // true value, use pointer coords and element rect if (value === true) { // if dimensions are negative, "switch" edges const width = is.number(rect.width) ? rect.width : rect.right - rect.left const height = is.number(rect.height) ? rect.height : rect.bottom - rect.top // don't use margin greater than half the relevent dimension margin = Math.min(margin, Math.abs((name === 'left' || name === 'right' ? width : height) / 2)) if (width < 0) { if (name === 'left') { name = 'right' } else if (name === 'right') { name = 'left' } } if (height < 0) { if (name === 'top') { name = 'bottom' } else if (name === 'bottom') { name = 'top' } } if (name === 'left') { const edge = width >= 0 ? rect.left : rect.right return page.x < edge + margin } if (name === 'top') { const edge = height >= 0 ? rect.top : rect.bottom return page.y < edge + margin } if (name === 'right') { return page.x > (width >= 0 ? rect.right : rect.left) - margin } if (name === 'bottom') { return page.y > (height >= 0 ? rect.bottom : rect.top) - margin } } // the remaining checks require an element if (!is.element(element)) { return false } return is.element(value) ? // the value is an element to use as a resize handle value === element : // otherwise check if element matches value as selector dom.matchesUpTo(element, value, interactableElement) } /* eslint-disable multiline-ternary */ // eslint-disable-next-line @typescript-eslint/consistent-type-imports function initCursors(browser: typeof import('@interactjs/utils/browser').default) { return browser.isIe9 ? { x: 'e-resize', y: 's-resize', xy: 'se-resize', top: 'n-resize', left: 'w-resize', bottom: 's-resize', right: 'e-resize', topleft: 'se-resize', bottomright: 'se-resize', topright: 'ne-resize', bottomleft: 'ne-resize', } : { x: 'ew-resize', y: 'ns-resize', xy: 'nwse-resize', top: 'ns-resize', left: 'ew-resize', bottom: 'ns-resize', right: 'ew-resize', topleft: 'nwse-resize', bottomright: 'nwse-resize', topright: 'nesw-resize', bottomleft: 'nesw-resize', } } /* eslint-enable multiline-ternary */ function start({ iEvent, interaction }: { iEvent: InteractEvent; interaction: Interaction }) { if (interaction.prepared.name !== 'resize' || !interaction.prepared.edges) { return } const resizeEvent = iEvent as ResizeEvent const rect = interaction.rect interaction._rects = { start: extend({}, rect), corrected: extend({}, rect), previous: extend({}, rect), delta: { left: 0, right: 0, width: 0, top: 0, bottom: 0, height: 0, }, } resizeEvent.edges = interaction.prepared.edges resizeEvent.rect = interaction._rects.corrected resizeEvent.deltaRect = interaction._rects.delta } function move({ iEvent, interaction }: { iEvent: InteractEvent; interaction: Interaction }) { if (interaction.prepared.name !== 'resize' || !interaction.prepared.edges) return const resizeEvent = iEvent as ResizeEvent const resizeOptions = interaction.interactable.options.resize const invert = resizeOptions.invert const invertible = invert === 'reposition' || invert === 'negate' const current = interaction.rect const { start: startRect, corrected, delta: deltaRect, previous } = interaction._rects extend(previous, corrected) if (invertible) { // if invertible, copy the current rect extend(corrected, current) if (invert === 'reposition') { // swap edge values if necessary to keep width/height positive if (corrected.top > corrected.bottom) { const swap = corrected.top corrected.top = corrected.bottom corrected.bottom = swap } if (corrected.left > corrected.right) { const swap = corrected.left corrected.left = corrected.right corrected.right = swap } } } else { // if not invertible, restrict to minimum of 0x0 rect corrected.top = Math.min(current.top, startRect.bottom) corrected.bottom = Math.max(current.bottom, startRect.top) corrected.left = Math.min(current.left, startRect.right) corrected.right = Math.max(current.right, startRect.left) } corrected.width = corrected.right - corrected.left corrected.height = corrected.bottom - corrected.top for (const edge in corrected) { deltaRect[edge] = corrected[edge] - previous[edge] } resizeEvent.edges = interaction.prepared.edges resizeEvent.rect = corrected resizeEvent.deltaRect = deltaRect } function end({ iEvent, interaction }: { iEvent: InteractEvent; interaction: Interaction }) { if (interaction.prepared.name !== 'resize' || !interaction.prepared.edges) return const resizeEvent = iEvent as ResizeEvent resizeEvent.edges = interaction.prepared.edges resizeEvent.rect = interaction._rects.corrected resizeEvent.deltaRect = interaction._rects.delta } function updateEventAxes({ iEvent, interaction, }: { iEvent: InteractEvent interaction: Interaction }) { if (interaction.prepared.name !== 'resize' || !interaction.resizeAxes) return const options = interaction.interactable.options const resizeEvent = iEvent as ResizeEvent if (options.resize.square) { if (interaction.resizeAxes === 'y') { resizeEvent.delta.x = resizeEvent.delta.y } else { resizeEvent.delta.y = resizeEvent.delta.x } resizeEvent.axes = 'xy' } else { resizeEvent.axes = interaction.resizeAxes if (interaction.resizeAxes === 'x') { resizeEvent.delta.y = 0 } else if (interaction.resizeAxes === 'y') { resizeEvent.delta.x = 0 } } } const resize: Plugin = { id: 'actions/resize', before: ['actions/drag'], install, listeners: { 'interactions:new': ({ interaction }) => { interaction.resizeAxes = 'xy' }, 'interactions:action-start': (arg) => { start(arg) updateEventAxes(arg) }, 'interactions:action-move': (arg) => { move(arg) updateEventAxes(arg) }, 'interactions:action-end': end, 'auto-start:check': resizeChecker, }, defaults: { square: false, preserveAspectRatio: false, axis: 'xy', // use default margin margin: NaN, // object with props left, right, top, bottom which are // true/false values to resize when the pointer is over that edge, // CSS selectors to match the handles for each direction // or the Elements for each handle edges: null, // a value of 'none' will limit the resize rect to a minimum of 0x0 // 'negate' will alow the rect to have negative width/height // 'reposition' will keep the width/height positive by swapping // the top and bottom edges and/or swapping the left and right edges invert: 'none', } as ResizableOptions, cursors: null as ReturnType, getCursor({ edges, axis, name }: ActionProps) { const cursors = resize.cursors let result: string = null if (axis) { result = cursors[name + axis] } else if (edges) { let cursorKey = '' for (const edge of ['top', 'bottom', 'left', 'right']) { if (edges[edge]) { cursorKey += edge } } result = cursors[cursorKey] } return result }, filterEventType: (type: string) => type.search('resize') === 0, defaultMargin: null as number, } export default resize ================================================ FILE: packages/@interactjs/actions/resize/resize.spec.ts ================================================ import * as helpers from '@interactjs/core/tests/_helpers' import type { ResizeEvent } from './plugin' import resize from './plugin' const { ltrbwh } = helpers describe('actions/resize', () => { test('action init', () => { const { scope } = helpers.testEnv({ plugins: [resize], }) expect(scope.actions.map.resize).toBeTruthy() expect(scope.actions.methodDict.resize).toBe('resizable') expect(scope.Interactable.prototype.resizable).toEqual(expect.any(Function)) }) test('checker', () => { const rect = Object.freeze({ left: 0, top: 0, right: 10, bottom: 10, width: 10, height: 10 }) const { scope, interactable, interaction, coords, target, down, start, move } = helpers.testEnv({ plugins: [resize], rect, }) const element = target as HTMLElement const checkArg = { action: null, interactable, interaction, element, rect, buttons: 0, } interactable.resizable({ edges: { left: true, top: true, right: true, bottom: true }, // use margin greater than width and height margin: Infinity, }) // resize top left down() scope.fire('auto-start:check', checkArg) expect(checkArg.action).toEqual({ name: 'resize', edges: { left: true, top: true, right: false, bottom: false }, }) // resize top right coords.page.x = 10 move() scope.fire('auto-start:check', checkArg) expect(checkArg.action).toEqual({ name: 'resize', edges: { left: false, top: true, right: true, bottom: false }, }) // resize bottom right coords.page.y = 10 move() scope.fire('auto-start:check', checkArg) expect(checkArg.action).toEqual({ name: 'resize', edges: { left: false, top: false, right: true, bottom: true }, }) const zeroRect = { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 } let resizeEvent: ResizeEvent = null interactable.on('resizestart resizemove resizeend', (e) => { resizeEvent = e }) coords.page.x = rect.right coords.page.y = rect.bottom down() start({ name: 'resize', edges: { bottom: true, right: true } }) // sets starting correct interaction._rects expect(interaction._rects).toEqual({ start: rect, corrected: rect, previous: rect, delta: zeroRect, }) // sets starting correct interaction.rect expect(interaction.rect).toEqual(rect) // resizestart event has extra resize props expect(hasResizeProps(resizeEvent)).toBe(true) coords.page.x = -100 coords.page.y = -200 resizeEvent = null move() // `invert: 'none'` interaction._rects are correct expect(interaction._rects).toEqual({ start: rect, corrected: zeroRect, previous: rect, delta: ltrbwh(0, 0, -rect.width, -rect.bottom, -rect.width, -rect.height), }) // `invert: 'none'` interaction.rect is correct expect(interaction.rect).toEqual(ltrbwh(0, 0, -100, -200, -100, -200)) // resizemove event has extra resize props expect(hasResizeProps(resizeEvent)).toBe(true) interactable.options.resize.invert = 'reposition' interaction.move() // `invert: 'reposition'` interaction._rects expect(interaction._rects).toEqual({ start: rect, corrected: ltrbwh(-100, -200, 0, 0, 100, 200), previous: interaction._rects.previous, // not testing previous delta: ltrbwh(-100, -200, 0, 0, 100, 200), }) interaction.move() interactable.options.resize.invert = 'negate' interaction.move() // invert: 'negate' interaction._rects expect(interaction._rects).toEqual({ start: rect, corrected: ltrbwh(0, 0, -100, -200, -100, -200), previous: interaction._rects.previous, // not testing previous delta: ltrbwh(100, 200, -100, -200, -200, -400), }) resizeEvent = null interaction.end() // resizeend event has extra resize props expect(hasResizeProps(resizeEvent)).toBe(true) }) }) function hasResizeProps(event: ResizeEvent) { return !!(event.deltaRect && event.rect && event.edges) } ================================================ FILE: packages/@interactjs/auto-scroll/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/auto-scroll/package.json ================================================ { "name": "@interactjs/auto-scroll", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/auto-scroll" }, "peerDependencies": { "@interactjs/utils": "1.10.27" }, "optionalDependencies": { "@interactjs/interact": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/auto-scroll/plugin.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type Interaction from '@interactjs/core/Interaction' import type { Scope, Plugin } from '@interactjs/core/scope' import type { ActionName, PointerType } from '@interactjs/core/types' import * as domUtils from '@interactjs/utils/domUtils' import is from '@interactjs/utils/is' import raf from '@interactjs/utils/raf' import { getStringOptionResult } from '@interactjs/utils/rect' import { getWindow } from '@interactjs/utils/window' declare module '@interactjs/core/scope' { interface Scope { autoScroll: typeof autoScroll } } declare module '@interactjs/core/Interaction' { interface Interaction { autoScroll?: typeof autoScroll } } declare module '@interactjs/core/options' { interface PerActionDefaults { autoScroll?: AutoScrollOptions } } export interface AutoScrollOptions { container?: Window | HTMLElement | string margin?: number distance?: number interval?: number speed?: number enabled?: boolean } function install(scope: Scope) { const { defaults, actions } = scope scope.autoScroll = autoScroll autoScroll.now = () => scope.now() actions.phaselessTypes.autoscroll = true defaults.perAction.autoScroll = autoScroll.defaults } const autoScroll = { defaults: { enabled: false, margin: 60, // the item that is scrolled (Window or HTMLElement) container: null as AutoScrollOptions['container'], // the scroll speed in pixels per second speed: 300, } as AutoScrollOptions, now: Date.now, interaction: null as Interaction | null, i: 0, // the handle returned by window.setInterval // Direction each pulse is to scroll in x: 0, y: 0, isScrolling: false, prevTime: 0, margin: 0, speed: 0, start(interaction: Interaction) { autoScroll.isScrolling = true raf.cancel(autoScroll.i) interaction.autoScroll = autoScroll autoScroll.interaction = interaction autoScroll.prevTime = autoScroll.now() autoScroll.i = raf.request(autoScroll.scroll) }, stop() { autoScroll.isScrolling = false if (autoScroll.interaction) { autoScroll.interaction.autoScroll = null } raf.cancel(autoScroll.i) }, // scroll the window by the values in scroll.x/y scroll() { const { interaction } = autoScroll const { interactable, element } = interaction const actionName = interaction.prepared.name const options = interactable.options[actionName].autoScroll const container = getContainer(options.container, interactable, element) const now = autoScroll.now() // change in time in seconds const dt = (now - autoScroll.prevTime) / 1000 // displacement const s = options.speed * dt if (s >= 1) { const scrollBy = { x: autoScroll.x * s, y: autoScroll.y * s, } if (scrollBy.x || scrollBy.y) { const prevScroll = getScroll(container) if (is.window(container)) { container.scrollBy(scrollBy.x, scrollBy.y) } else if (container) { container.scrollLeft += scrollBy.x container.scrollTop += scrollBy.y } const curScroll = getScroll(container) const delta = { x: curScroll.x - prevScroll.x, y: curScroll.y - prevScroll.y, } if (delta.x || delta.y) { interactable.fire({ type: 'autoscroll', target: element, interactable, delta, interaction, container, }) } } autoScroll.prevTime = now } if (autoScroll.isScrolling) { raf.cancel(autoScroll.i) autoScroll.i = raf.request(autoScroll.scroll) } }, check(interactable: Interactable, actionName: ActionName) { const options = interactable.options return options[actionName].autoScroll?.enabled }, onInteractionMove({ interaction, pointer, }: { interaction: Interaction pointer: PointerType }) { if ( !(interaction.interacting() && autoScroll.check(interaction.interactable, interaction.prepared.name)) ) { return } if (interaction.simulation) { autoScroll.x = autoScroll.y = 0 return } let top: boolean let right: boolean let bottom: boolean let left: boolean const { interactable, element } = interaction const actionName = interaction.prepared.name const options = interactable.options[actionName].autoScroll const container = getContainer(options.container, interactable, element) if (is.window(container)) { left = pointer.clientX < autoScroll.margin top = pointer.clientY < autoScroll.margin right = pointer.clientX > container.innerWidth - autoScroll.margin bottom = pointer.clientY > container.innerHeight - autoScroll.margin } else { const rect = domUtils.getElementClientRect(container) left = pointer.clientX < rect.left + autoScroll.margin top = pointer.clientY < rect.top + autoScroll.margin right = pointer.clientX > rect.right - autoScroll.margin bottom = pointer.clientY > rect.bottom - autoScroll.margin } autoScroll.x = right ? 1 : left ? -1 : 0 autoScroll.y = bottom ? 1 : top ? -1 : 0 if (!autoScroll.isScrolling) { // set the autoScroll properties to those of the target autoScroll.margin = options.margin autoScroll.speed = options.speed autoScroll.start(interaction) } }, } export function getContainer(value: any, interactable: Interactable, element: Element) { return ( (is.string(value) ? getStringOptionResult(value, interactable, element) : value) || getWindow(element) ) } export function getScroll(container: any) { if (is.window(container)) { container = window.document.body } return { x: container.scrollLeft, y: container.scrollTop } } export function getScrollSize(container: any) { if (is.window(container)) { container = window.document.body } return { x: container.scrollWidth, y: container.scrollHeight } } export function getScrollSizeDelta( { interaction, element, }: { interaction: Partial> element: Element }, func: any, ) { const scrollOptions = interaction && interaction.interactable.options[interaction.prepared.name].autoScroll if (!scrollOptions || !scrollOptions.enabled) { func() return { x: 0, y: 0 } } const scrollContainer = getContainer(scrollOptions.container, interaction.interactable, element) const prevSize = getScroll(scrollContainer) func() const curSize = getScroll(scrollContainer) return { x: curSize.x - prevSize.x, y: curSize.y - prevSize.y, } } const autoScrollPlugin: Plugin = { id: 'auto-scroll', install, listeners: { 'interactions:new': ({ interaction }) => { interaction.autoScroll = null }, 'interactions:destroy': ({ interaction }) => { interaction.autoScroll = null autoScroll.stop() if (autoScroll.interaction) { autoScroll.interaction = null } }, 'interactions:stop': autoScroll.stop, 'interactions:action-move': (arg: any) => autoScroll.onInteractionMove(arg), }, } export default autoScrollPlugin ================================================ FILE: packages/@interactjs/auto-start/InteractableMethods.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { Interaction } from '@interactjs/core/Interaction' import type { Scope } from '@interactjs/core/scope' import type { ActionProps, PointerType, PointerEventType, Element } from '@interactjs/core/types' import is from '@interactjs/utils/is' import { warnOnce } from '@interactjs/utils/misc' declare module '@interactjs/core/Interactable' { interface Interactable { getAction: ( this: Interactable, pointer: PointerType, event: PointerEventType, interaction: Interaction, element: Element, ) => ActionProps | null styleCursor(newValue: boolean): this styleCursor(): boolean /** * Returns or sets whether the the cursor should be changed depending on the * action that would be performed if the mouse were pressed and dragged. * * @param {boolean} [newValue] * @return {boolean | Interactable} The current setting or this Interactable */ styleCursor(newValue?: boolean): boolean | this actionChecker(checker: Function): Interactable actionChecker(): Function /** * ```js * interact('.resize-drag') * .resizable(true) * .draggable(true) * .actionChecker(function (pointer, event, action, interactable, element, interaction) { * * if (interact.matchesSelector(event.target, '.drag-handle')) { * // force drag with handle target * action.name = drag * } * else { * // resize from the top and right edges * action.name = 'resize' * action.edges = { top: true, right: true } * } * * return action * }) * ``` * * Returns or sets the function used to check action to be performed on * pointerDown * * @param checker - A function which takes a pointer event, * defaultAction string, interactable, element and interaction as parameters * and returns an object with name property 'drag' 'resize' or 'gesture' and * optionally an `edges` object with boolean 'top', 'left', 'bottom' and right * props. * @returns The checker function or this Interactable */ actionChecker(checker?: Function): Interactable | Function /** @returns This interactable */ ignoreFrom(newValue: string | Element | null): Interactable /** @returns The current ignoreFrom value */ ignoreFrom(): string | Element | null /** * If the target of the `mousedown`, `pointerdown` or `touchstart` event or any * of it's parents match the given CSS selector or Element, no * drag/resize/gesture is started. * * @deprecated * Don't use this method. Instead set the `ignoreFrom` option for each action * or for `pointerEvents` * * ```js * interact(targett) * .draggable({ * ignoreFrom: 'input, textarea, a[href]'', * }) * .pointerEvents({ * ignoreFrom: '[no-pointer]', * }) * ``` * Interactable */ ignoreFrom( /** a CSS selector string, an Element or `null` to not ignore any elements */ newValue?: string | Element | null, ): Interactable | string | Element | null allowFrom(): boolean /** * * A drag/resize/gesture is started only If the target of the `mousedown`, * `pointerdown` or `touchstart` event or any of it's parents match the given * CSS selector or Element. * * @deprecated * Don't use this method. Instead set the `allowFrom` option for each action * or for `pointerEvents` * * ```js * interact(targett) * .resizable({ * allowFrom: '.resize-handle', * .pointerEvents({ * allowFrom: '.handle',, * }) * ``` * * @param {string | Element | null} [newValue] * @return {string | Element | object} The current allowFrom value or this * Interactable */ allowFrom( /** A CSS selector string, an Element or `null` to allow from any element */ newValue: string | Element | null, ): Interactable } } function install(scope: Scope) { const { Interactable, // tslint:disable-line no-shadowed-variable } = scope Interactable.prototype.getAction = function getAction( this: Interactable, pointer: PointerType, event: PointerEventType, interaction: Interaction, element: Element, ): ActionProps { const action = defaultActionChecker(this, event, interaction, element, scope) if (this.options.actionChecker) { return this.options.actionChecker(pointer, event, action, this, element, interaction) } return action } Interactable.prototype.ignoreFrom = warnOnce(function (this: Interactable, newValue) { return this._backCompatOption('ignoreFrom', newValue) }, 'Interactable.ignoreFrom() has been deprecated. Use Interactble.draggable({ignoreFrom: newValue}).') Interactable.prototype.allowFrom = warnOnce(function (this: Interactable, newValue) { return this._backCompatOption('allowFrom', newValue) }, 'Interactable.allowFrom() has been deprecated. Use Interactble.draggable({allowFrom: newValue}).') Interactable.prototype.actionChecker = actionChecker Interactable.prototype.styleCursor = styleCursor } function defaultActionChecker( interactable: Interactable, event: PointerEventType, interaction: Interaction, element: Element, scope: Scope, ) { const rect = interactable.getRect(element) const buttons = (event as MouseEvent).buttons || { 0: 1, 1: 4, 3: 8, 4: 16, }[(event as MouseEvent).button as 0 | 1 | 3 | 4] const arg = { action: null, interactable, interaction, element, rect, buttons, } scope.fire('auto-start:check', arg) return arg.action } function styleCursor(this: Interactable, newValue?: boolean) { if (is.bool(newValue)) { this.options.styleCursor = newValue return this } if (newValue === null) { delete this.options.styleCursor return this } return this.options.styleCursor } function actionChecker(this: Interactable, checker?: any) { if (is.func(checker)) { this.options.actionChecker = checker return this } if (checker === null) { delete this.options.actionChecker return this } return this.options.actionChecker } export default { id: 'auto-start/interactableMethods', install, } ================================================ FILE: packages/@interactjs/auto-start/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/auto-start/autoStart.spec.ts ================================================ import drag from '@interactjs/actions/drag/plugin' import * as helpers from '@interactjs/core/tests/_helpers' import autoStart from './base' test('autoStart', () => { window.PointerEvent = null document.body.innerHTML = `
` Object.assign(document.body.style) const { interaction, interactable, event, coords, target: element, down, } = helpers.testEnv({ plugins: [autoStart, drag], target: document.getElementById('target'), }) interactable.draggable(true) interaction.pointerType = coords.pointerType = 'mouse' coords.buttons = 1 down() // prepares action expect(interaction.prepared).toEqual({ name: 'drag', axis: 'xy', edges: undefined }) // set interaction.rect expect(interaction.rect).toEqual( helpers.getProps(element.getBoundingClientRect(), ['top', 'left', 'bottom', 'right', 'width', 'height']), ) // sets drag cursor expect(element.style.cursor).toBe('move') const cursorChecker = jest.fn(() => 'pointer') interactable.draggable({ cursorChecker, }) interaction.pointerDown(event, event, element) // calls cursorChecker with expected args expect(cursorChecker).toHaveBeenCalledWith( { name: 'drag', axis: 'xy', edges: undefined }, interactable, element, false, ) interaction.pointerDown(event, event, element) // uses cursorChecker value expect(element.style.cursor).toBe('pointer') coords.page.x += 10 coords.client.x += 10 interaction.pointerMove(event, event, element) // down -> move starts action expect(interaction._interacting).toBe(true) // calls cursorChecker with true for interacting arg expect(cursorChecker).toHaveBeenCalledWith( { name: 'drag', axis: 'xy', edges: undefined }, interactable, element, true, ) }) ================================================ FILE: packages/@interactjs/auto-start/base.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { Interaction } from '@interactjs/core/Interaction' import type { Scope, SignalArgs, Plugin } from '@interactjs/core/scope' import type { CursorChecker, PointerType, PointerEventType, Element, ActionName, ActionProps, } from '@interactjs/core/types' import * as domUtils from '@interactjs/utils/domUtils' import extend from '@interactjs/utils/extend' import is from '@interactjs/utils/is' import { copyAction } from '@interactjs/utils/misc' /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import './InteractableMethods' import InteractableMethods from './InteractableMethods' /* eslint-enable import/no-duplicates */ declare module '@interactjs/core/InteractStatic' { export interface InteractStatic { /** * Returns or sets the maximum number of concurrent interactions allowed. By * default only 1 interaction is allowed at a time (for backwards * compatibility). To allow multiple interactions on the same Interactables and * elements, you need to enable it in the draggable, resizable and gesturable * `'max'` and `'maxPerElement'` options. * * @param {number} [newValue] Any number. newValue <= 0 means no interactions. */ maxInteractions: (newValue: any) => any } } declare module '@interactjs/core/scope' { interface Scope { autoStart: AutoStart } interface SignalArgs { 'autoStart:before-start': Omit & { interaction: Interaction } 'autoStart:prepared': { interaction: Interaction } 'auto-start:check': CheckSignalArg } } declare module '@interactjs/core/options' { interface BaseDefaults { actionChecker?: any cursorChecker?: any styleCursor?: any } interface PerActionDefaults { manualStart?: boolean max?: number maxPerElement?: number allowFrom?: string | Element ignoreFrom?: string | Element cursorChecker?: CursorChecker // only allow left button by default // see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons#Return_value // TODO: docst mouseButtons?: 0 | 1 | 2 | 4 | 8 | 16 } } interface CheckSignalArg { interactable: Interactable interaction: Interaction element: Element action: ActionProps buttons: number } export interface AutoStart { // Allow this many interactions to happen simultaneously maxInteractions: number withinInteractionLimit: typeof withinInteractionLimit cursorElement: Element } function install(scope: Scope) { const { interactStatic: interact, defaults } = scope scope.usePlugin(InteractableMethods) defaults.base.actionChecker = null defaults.base.styleCursor = true extend(defaults.perAction, { manualStart: false, max: Infinity, maxPerElement: 1, allowFrom: null, ignoreFrom: null, // only allow left button by default // see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons#Return_value mouseButtons: 1, }) interact.maxInteractions = (newValue: number) => maxInteractions(newValue, scope) scope.autoStart = { // Allow this many interactions to happen simultaneously maxInteractions: Infinity, withinInteractionLimit, cursorElement: null, } } function prepareOnDown( { interaction, pointer, event, eventTarget }: SignalArgs['interactions:down'], scope: Scope, ) { if (interaction.interacting()) return const actionInfo = getActionInfo(interaction, pointer, event, eventTarget, scope) prepare(interaction, actionInfo, scope) } function prepareOnMove( { interaction, pointer, event, eventTarget }: SignalArgs['interactions:move'], scope: Scope, ) { if (interaction.pointerType !== 'mouse' || interaction.pointerIsDown || interaction.interacting()) return const actionInfo = getActionInfo(interaction, pointer, event, eventTarget as Element, scope) prepare(interaction, actionInfo, scope) } function startOnMove(arg: SignalArgs['interactions:move'], scope: Scope) { const { interaction } = arg if ( !interaction.pointerIsDown || interaction.interacting() || !interaction.pointerWasMoved || !interaction.prepared.name ) { return } scope.fire('autoStart:before-start', arg) const { interactable } = interaction const actionName = (interaction as Interaction).prepared.name if (actionName && interactable) { // check manualStart and interaction limit if ( interactable.options[actionName].manualStart || !withinInteractionLimit(interactable, interaction.element, interaction.prepared, scope) ) { interaction.stop() } else { interaction.start(interaction.prepared, interactable, interaction.element) setInteractionCursor(interaction, scope) } } } function clearCursorOnStop({ interaction }: { interaction: Interaction }, scope: Scope) { const { interactable } = interaction if (interactable && interactable.options.styleCursor) { setCursor(interaction.element, '', scope) } } // Check if the current interactable supports the action. // If so, return the validated action. Otherwise, return null function validateAction( action: ActionProps, interactable: Interactable, element: Element, eventTarget: Node, scope: Scope, ) { if ( interactable.testIgnoreAllow(interactable.options[action.name], element, eventTarget) && interactable.options[action.name].enabled && withinInteractionLimit(interactable, element, action, scope) ) { return action } return null } function validateMatches( interaction: Interaction, pointer: PointerType, event: PointerEventType, matches: Interactable[], matchElements: Element[], eventTarget: Node, scope: Scope, ) { for (let i = 0, len = matches.length; i < len; i++) { const match = matches[i] const matchElement = matchElements[i] const matchAction = match.getAction(pointer, event, interaction, matchElement) if (!matchAction) { continue } const action = validateAction(matchAction, match, matchElement, eventTarget, scope) if (action) { return { action, interactable: match, element: matchElement, } } } return { action: null, interactable: null, element: null } } function getActionInfo( interaction: Interaction, pointer: PointerType, event: PointerEventType, eventTarget: Node, scope: Scope, ) { let matches: Interactable[] = [] let matchElements: Element[] = [] let element = eventTarget as Element function pushMatches(interactable: Interactable) { matches.push(interactable) matchElements.push(element) } while (is.element(element)) { matches = [] matchElements = [] scope.interactables.forEachMatch(element, pushMatches) const actionInfo = validateMatches( interaction, pointer, event, matches, matchElements, eventTarget, scope, ) if (actionInfo.action && !actionInfo.interactable.options[actionInfo.action.name].manualStart) { return actionInfo } element = domUtils.parentNode(element) as Element } return { action: null, interactable: null, element: null } } function prepare( interaction: Interaction, { action, interactable, element, }: { action: ActionProps interactable: Interactable element: Element }, scope: Scope, ) { action = action || { name: null } interaction.interactable = interactable interaction.element = element copyAction(interaction.prepared, action) interaction.rect = interactable && action.name ? interactable.getRect(element) : null setInteractionCursor(interaction, scope) scope.fire('autoStart:prepared', { interaction }) } function withinInteractionLimit( interactable: Interactable, element: Element, action: ActionProps, scope: Scope, ) { const options = interactable.options const maxActions = options[action.name].max const maxPerElement = options[action.name].maxPerElement const autoStartMax = scope.autoStart.maxInteractions let activeInteractions = 0 let interactableCount = 0 let elementCount = 0 // no actions if any of these values == 0 if (!(maxActions && maxPerElement && autoStartMax)) { return false } for (const interaction of scope.interactions.list) { const otherAction = interaction.prepared.name if (!interaction.interacting()) { continue } activeInteractions++ if (activeInteractions >= autoStartMax) { return false } if (interaction.interactable !== interactable) { continue } interactableCount += otherAction === action.name ? 1 : 0 if (interactableCount >= maxActions) { return false } if (interaction.element === element) { elementCount++ if (otherAction === action.name && elementCount >= maxPerElement) { return false } } } return autoStartMax > 0 } function maxInteractions(newValue: any, scope: Scope) { if (is.number(newValue)) { scope.autoStart.maxInteractions = newValue return this } return scope.autoStart.maxInteractions } function setCursor(element: Element, cursor: string, scope: Scope) { const { cursorElement: prevCursorElement } = scope.autoStart if (prevCursorElement && prevCursorElement !== element) { prevCursorElement.style.cursor = '' } element.ownerDocument.documentElement.style.cursor = cursor element.style.cursor = cursor scope.autoStart.cursorElement = cursor ? element : null } function setInteractionCursor(interaction: Interaction, scope: Scope) { const { interactable, element, prepared } = interaction if (!(interaction.pointerType === 'mouse' && interactable && interactable.options.styleCursor)) { // clear previous target element cursor if (scope.autoStart.cursorElement) { setCursor(scope.autoStart.cursorElement, '', scope) } return } let cursor = '' if (prepared.name) { const cursorChecker = interactable.options[prepared.name].cursorChecker if (is.func(cursorChecker)) { cursor = cursorChecker(prepared, interactable, element, interaction._interacting) } else { cursor = scope.actions.map[prepared.name].getCursor(prepared) } } setCursor(interaction.element, cursor || '', scope) } const autoStart: Plugin = { id: 'auto-start/base', before: ['actions'], install, listeners: { 'interactions:down': prepareOnDown, 'interactions:move': (arg, scope) => { prepareOnMove(arg, scope) startOnMove(arg, scope) }, 'interactions:stop': clearCursorOnStop, }, maxInteractions, withinInteractionLimit, validateAction, } export default autoStart ================================================ FILE: packages/@interactjs/auto-start/dragAxis.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type Interaction from '@interactjs/core/Interaction' import type { SignalArgs, Scope } from '@interactjs/core/scope' import type { ActionName, Element } from '@interactjs/core/types' import { parentNode } from '@interactjs/utils/domUtils' import is from '@interactjs/utils/is' import autoStart from './base' function beforeStart({ interaction, eventTarget, dx, dy }: SignalArgs['interactions:move'], scope: Scope) { if (interaction.prepared.name !== 'drag') return // check if a drag is in the correct axis const absX = Math.abs(dx) const absY = Math.abs(dy) const targetOptions = interaction.interactable.options.drag const startAxis = targetOptions.startAxis const currentAxis = absX > absY ? 'x' : absX < absY ? 'y' : 'xy' interaction.prepared.axis = targetOptions.lockAxis === 'start' ? (currentAxis[0] as 'x' | 'y') // always lock to one axis even if currentAxis === 'xy' : targetOptions.lockAxis // if the movement isn't in the startAxis of the interactable if (currentAxis !== 'xy' && startAxis !== 'xy' && startAxis !== currentAxis) { // cancel the prepared action ;(interaction as Interaction).prepared.name = null // then try to get a drag from another ineractable let element = eventTarget as Element const getDraggable = function (interactable: Interactable): Interactable | void { if (interactable === interaction.interactable) return const options = interaction.interactable.options.drag if (!options.manualStart && interactable.testIgnoreAllow(options, element, eventTarget)) { const action = interactable.getAction( interaction.downPointer, interaction.downEvent, interaction, element, ) if ( action && action.name === 'drag' && checkStartAxis(currentAxis, interactable) && autoStart.validateAction(action, interactable, element, eventTarget, scope) ) { return interactable } } } // check all interactables while (is.element(element)) { const interactable = scope.interactables.forEachMatch(element, getDraggable) if (interactable) { ;(interaction as Interaction).prepared.name = 'drag' interaction.interactable = interactable interaction.element = element break } element = parentNode(element) as Element } } } function checkStartAxis(startAxis: string, interactable: Interactable) { if (!interactable) { return false } const thisAxis = interactable.options.drag.startAxis return startAxis === 'xy' || thisAxis === 'xy' || thisAxis === startAxis } export default { id: 'auto-start/dragAxis', listeners: { 'autoStart:before-start': beforeStart }, } ================================================ FILE: packages/@interactjs/auto-start/hold.spec.ts ================================================ import * as helpers from '@interactjs/core/tests/_helpers' import hold from './hold' test('autoStart/hold', () => { const { scope } = helpers.testEnv({ plugins: [hold] }) // sets scope.defaults.perAction.hold expect(scope.defaults.perAction.hold).toBe(0) // backwards compatible "delay" alias. expect(scope.defaults.perAction.delay).toBe(0) const holdDuration = 1000 const actionName = 'TEST_ACTION' const interaction: any = { interactable: { options: { [actionName]: { hold: holdDuration } } }, prepared: { name: actionName }, } // gets holdDuration expect(hold.getHoldDuration(interaction)).toBe(holdDuration) const delayDuration = 500 interaction.interactable.options[actionName].delay = delayDuration delete interaction.interactable.options[actionName].hold // gets holdDuration from "delay" value expect(hold.getHoldDuration(interaction)).toBe(delayDuration) }) ================================================ FILE: packages/@interactjs/auto-start/hold.ts ================================================ import type Interaction from '@interactjs/core/Interaction' import type { Scope, Plugin } from '@interactjs/core/scope' /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import './base' import basePlugin from './base' /* eslint-enable */ declare module '@interactjs/core/options' { interface PerActionDefaults { hold?: number delay?: number } } declare module '@interactjs/core/Interaction' { interface Interaction { autoStartHoldTimer?: any } } function install(scope: Scope) { const { defaults } = scope scope.usePlugin(basePlugin) defaults.perAction.hold = 0 defaults.perAction.delay = 0 } function getHoldDuration(interaction: Interaction) { const actionName = interaction.prepared && interaction.prepared.name if (!actionName) { return null } const options = interaction.interactable.options return options[actionName].hold || options[actionName].delay } const hold: Plugin = { id: 'auto-start/hold', install, listeners: { 'interactions:new': ({ interaction }) => { interaction.autoStartHoldTimer = null }, 'autoStart:prepared': ({ interaction }) => { const hold = getHoldDuration(interaction) if (hold > 0) { interaction.autoStartHoldTimer = setTimeout(() => { interaction.start(interaction.prepared, interaction.interactable, interaction.element) }, hold) } }, 'interactions:move': ({ interaction, duplicate }) => { if (interaction.autoStartHoldTimer && interaction.pointerWasMoved && !duplicate) { clearTimeout(interaction.autoStartHoldTimer) interaction.autoStartHoldTimer = null } }, // prevent regular down->move autoStart 'autoStart:before-start': ({ interaction }) => { const holdDuration = getHoldDuration(interaction) if (holdDuration > 0) { interaction.prepared.name = null } }, }, getHoldDuration, } export default hold ================================================ FILE: packages/@interactjs/auto-start/package.json ================================================ { "name": "@interactjs/auto-start", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/auto-start" }, "peerDependencies": { "@interactjs/core": "1.10.27", "@interactjs/utils": "1.10.27" }, "optionalDependencies": { "@interactjs/interact": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/auto-start/plugin.ts ================================================ import type { Scope } from '@interactjs/core/scope' /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import './base' import './dragAxis' import './hold' import autoStart from './base' import dragAxis from './dragAxis' import hold from './hold' /* eslint-enable import/no-duplicates */ export default { id: 'auto-start', install(scope: Scope) { scope.usePlugin(autoStart) scope.usePlugin(hold) scope.usePlugin(dragAxis) }, } ================================================ FILE: packages/@interactjs/core/BaseEvent.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { Interaction, InteractionProxy } from '@interactjs/core/Interaction' import type { ActionName } from '@interactjs/core/types' export class BaseEvent { declare type: string declare target: EventTarget declare currentTarget: Node declare interactable: Interactable /** @internal */ declare _interaction: Interaction declare timeStamp: number immediatePropagationStopped = false propagationStopped = false constructor(interaction: Interaction) { this._interaction = interaction } preventDefault() {} /** * Don't call any other listeners (even on the current target) */ stopPropagation() { this.propagationStopped = true } /** * Don't call listeners on the remaining targets */ stopImmediatePropagation() { this.immediatePropagationStopped = this.propagationStopped = true } } // defined outside of class definition to avoid assignment of undefined during // construction export interface BaseEvent { interaction: InteractionProxy } // getters and setters defined here to support typescript 3.6 and below which // don't support getter and setters in .d.ts files Object.defineProperty(BaseEvent.prototype, 'interaction', { get(this: BaseEvent) { return this._interaction._proxy }, set(this: BaseEvent) {}, }) ================================================ FILE: packages/@interactjs/core/Eventable.spec.ts ================================================ import { Eventable } from './Eventable' test('core/Eventable', () => { const eventable = new Eventable() const type = 'TEST' const testEvent = { type, immediatePropagationStopped: false, } let firedEvent: any const listener = (event: any) => { firedEvent = event } eventable.on(type, listener) eventable.fire(testEvent) // on'd listener is called expect(firedEvent).toBe(testEvent) firedEvent = undefined eventable.off(type, listener) eventable.fire(testEvent) // off'd listener is not called expect(firedEvent).toBeUndefined() testEvent.immediatePropagationStopped = true eventable.on(type, listener) eventable.fire(testEvent) // listener is not called with immediatePropagationStopped expect(firedEvent).toBeUndefined() }) ================================================ FILE: packages/@interactjs/core/Eventable.ts ================================================ import * as arr from '@interactjs/utils/arr' import extend from '@interactjs/utils/extend' import type { NormalizedListeners } from '@interactjs/utils/normalizeListeners' import normalize from '@interactjs/utils/normalizeListeners' import type { Listener, ListenersArg, Rect } from '@interactjs/core/types' function fireUntilImmediateStopped(event: any, listeners: Listener[]) { for (const listener of listeners) { if (event.immediatePropagationStopped) { break } listener(event) } } export class Eventable { options: any types: NormalizedListeners = {} propagationStopped = false immediatePropagationStopped = false global: any constructor(options?: { [index: string]: any }) { this.options = extend({}, options || {}) } fire(event: T) { let listeners: Listener[] const global = this.global // Interactable#on() listeners // tslint:disable no-conditional-assignment if ((listeners = this.types[event.type])) { fireUntilImmediateStopped(event, listeners) } // interact.on() listeners if (!event.propagationStopped && global && (listeners = global[event.type])) { fireUntilImmediateStopped(event, listeners) } } on(type: string, listener: ListenersArg) { const listeners = normalize(type, listener) for (type in listeners) { this.types[type] = arr.merge(this.types[type] || [], listeners[type]) } } off(type: string, listener: ListenersArg) { const listeners = normalize(type, listener) for (type in listeners) { const eventList = this.types[type] if (!eventList || !eventList.length) { continue } for (const subListener of listeners[type]) { const index = eventList.indexOf(subListener) if (index !== -1) { eventList.splice(index, 1) } } } } getRect(_element: Element): Rect { return null } } ================================================ FILE: packages/@interactjs/core/InteractEvent.ts ================================================ import extend from '@interactjs/utils/extend' import getOriginXY from '@interactjs/utils/getOriginXY' import hypot from '@interactjs/utils/hypot' import type { Point, FullRect, PointerEventType, Element, ActionName } from '@interactjs/core/types' import { BaseEvent } from './BaseEvent' import type { Interaction } from './Interaction' import { defaults } from './options' export type EventPhase = keyof PhaseMap export interface PhaseMap { start: true move: true end: true } // defined outside of class definition to avoid assignment of undefined during // construction export interface InteractEvent { pageX: number pageY: number clientX: number clientY: number dx: number dy: number velocityX: number velocityY: number } export class InteractEvent< T extends ActionName = never, P extends EventPhase = EventPhase, > extends BaseEvent { declare target: Element declare currentTarget: Element relatedTarget: Element | null = null screenX?: number screenY?: number button: number buttons: number ctrlKey: boolean shiftKey: boolean altKey: boolean metaKey: boolean page: Point client: Point delta: Point rect: FullRect x0: number y0: number t0: number dt: number duration: number clientX0: number clientY0: number velocity: Point speed: number swipe: ReturnType['getSwipe']> // resize axes?: 'x' | 'y' | 'xy' /** @internal */ preEnd?: boolean constructor( interaction: Interaction, event: PointerEventType, actionName: T, phase: P, element: Element, preEnd?: boolean, type?: string, ) { super(interaction) element = element || interaction.element const target = interaction.interactable const deltaSource = (((target && target.options) || defaults) as any).deltaSource as 'page' | 'client' const origin = getOriginXY(target, element, actionName) const starting = phase === 'start' const ending = phase === 'end' const prevEvent = starting ? this : interaction.prevEvent const coords = starting ? interaction.coords.start : ending ? { page: prevEvent.page, client: prevEvent.client, timeStamp: interaction.coords.cur.timeStamp } : interaction.coords.cur this.page = extend({}, coords.page) this.client = extend({}, coords.client) this.rect = extend({}, interaction.rect) this.timeStamp = coords.timeStamp if (!ending) { this.page.x -= origin.x this.page.y -= origin.y this.client.x -= origin.x this.client.y -= origin.y } this.ctrlKey = event.ctrlKey this.altKey = event.altKey this.shiftKey = event.shiftKey this.metaKey = event.metaKey this.button = (event as MouseEvent).button this.buttons = (event as MouseEvent).buttons this.target = element this.currentTarget = element this.preEnd = preEnd this.type = type || actionName + (phase || '') this.interactable = target this.t0 = starting ? interaction.pointers[interaction.pointers.length - 1].downTime : prevEvent.t0 this.x0 = interaction.coords.start.page.x - origin.x this.y0 = interaction.coords.start.page.y - origin.y this.clientX0 = interaction.coords.start.client.x - origin.x this.clientY0 = interaction.coords.start.client.y - origin.y if (starting || ending) { this.delta = { x: 0, y: 0 } } else { this.delta = { x: this[deltaSource].x - prevEvent[deltaSource].x, y: this[deltaSource].y - prevEvent[deltaSource].y, } } this.dt = interaction.coords.delta.timeStamp this.duration = this.timeStamp - this.t0 // velocity and speed in pixels per second this.velocity = extend({}, interaction.coords.velocity[deltaSource]) this.speed = hypot(this.velocity.x, this.velocity.y) this.swipe = ending || phase === 'inertiastart' ? this.getSwipe() : null } getSwipe() { const interaction = this._interaction if (interaction.prevEvent.speed < 600 || this.timeStamp - interaction.prevEvent.timeStamp > 150) { return null } let angle = (180 * Math.atan2(interaction.prevEvent.velocityY, interaction.prevEvent.velocityX)) / Math.PI const overlap = 22.5 if (angle < 0) { angle += 360 } const left = 135 - overlap <= angle && angle < 225 + overlap const up = 225 - overlap <= angle && angle < 315 + overlap const right = !left && (315 - overlap <= angle || angle < 45 + overlap) const down = !up && 45 - overlap <= angle && angle < 135 + overlap return { up, down, left, right, angle, speed: interaction.prevEvent.speed, velocity: { x: interaction.prevEvent.velocityX, y: interaction.prevEvent.velocityY, }, } } preventDefault() {} /** * Don't call listeners on the remaining targets */ stopImmediatePropagation() { this.immediatePropagationStopped = this.propagationStopped = true } /** * Don't call any other listeners (even on the current target) */ stopPropagation() { this.propagationStopped = true } } // getters and setters defined here to support typescript 3.6 and below which // don't support getter and setters in .d.ts files Object.defineProperties(InteractEvent.prototype, { pageX: { get() { return this.page.x }, set(value) { this.page.x = value }, }, pageY: { get() { return this.page.y }, set(value) { this.page.y = value }, }, clientX: { get() { return this.client.x }, set(value) { this.client.x = value }, }, clientY: { get() { return this.client.y }, set(value) { this.client.y = value }, }, dx: { get() { return this.delta.x }, set(value) { this.delta.x = value }, }, dy: { get() { return this.delta.y }, set(value) { this.delta.y = value }, }, velocityX: { get() { return this.velocity.x }, set(value) { this.velocity.x = value }, }, velocityY: { get() { return this.velocity.y }, set(value) { this.velocity.y = value }, }, }) ================================================ FILE: packages/@interactjs/core/InteractStatic.ts ================================================ import browser from '@interactjs/utils/browser' import * as domUtils from '@interactjs/utils/domUtils' import is from '@interactjs/utils/is' import isNonNativeEvent from '@interactjs/utils/isNonNativeEvent' import { warnOnce } from '@interactjs/utils/misc' import * as pointerUtils from '@interactjs/utils/pointerUtils' import type { Scope, Plugin } from '@interactjs/core/scope' import type { Context, EventTypes, Listener, ListenersArg, Target } from '@interactjs/core/types' import type { Interactable } from './Interactable' import type { Options } from './options' /** * ```js * interact('#draggable').draggable(true) * * var rectables = interact('rect') * rectables * .gesturable(true) * .on('gesturemove', function (event) { * // ... * }) * ``` * * The methods of this variable can be used to set elements as interactables * and also to change various default settings. * * Calling it as a function and passing an element or a valid CSS selector * string returns an Interactable object which has various methods to configure * it. * * @param {Element | string} target The HTML or SVG Element to interact with * or CSS selector * @return {Interactable} */ export interface InteractStatic { (target: Target, options?: Options): Interactable getPointerAverage: typeof pointerUtils.pointerAverage getTouchBBox: typeof pointerUtils.touchBBox getTouchDistance: typeof pointerUtils.touchDistance getTouchAngle: typeof pointerUtils.touchAngle getElementRect: typeof domUtils.getElementRect getElementClientRect: typeof domUtils.getElementClientRect matchesSelector: typeof domUtils.matchesSelector closest: typeof domUtils.closest /** @internal */ globalEvents: any version: string /** @internal */ scope: Scope /** * Use a plugin */ use( plugin: Plugin, options?: { [key: string]: any }, ): any /** * Check if an element or selector has been set with the `interact(target)` * function * * @return {boolean} Indicates if the element or CSS selector was previously * passed to interact */ isSet( /* The Element or string being searched for */ target: Target, options?: any, ): boolean on(type: string | EventTypes, listener: ListenersArg, options?: object): any off(type: EventTypes, listener: any, options?: object): any debug(): any /** * Whether or not the browser supports touch input */ supportsTouch(): boolean /** * Whether or not the browser supports PointerEvents */ supportsPointerEvent(): boolean /** * Cancels all interactions (end events are not fired) */ stop(): InteractStatic /** * Returns or sets the distance the pointer must be moved before an action * sequence occurs. This also affects tolerance for tap events. */ pointerMoveTolerance( /** The movement from the start position must be greater than this value */ newValue?: number, ): InteractStatic | number addDocument(doc: Document, options?: object): void removeDocument(doc: Document): void } export function createInteractStatic(scope: Scope): InteractStatic { const interact = ((target: Target, options: Options) => { let interactable = scope.interactables.getExisting(target, options) if (!interactable) { interactable = scope.interactables.new(target, options) interactable.events.global = interact.globalEvents } return interactable }) as InteractStatic // expose the functions used to calculate multi-touch properties interact.getPointerAverage = pointerUtils.pointerAverage interact.getTouchBBox = pointerUtils.touchBBox interact.getTouchDistance = pointerUtils.touchDistance interact.getTouchAngle = pointerUtils.touchAngle interact.getElementRect = domUtils.getElementRect interact.getElementClientRect = domUtils.getElementClientRect interact.matchesSelector = domUtils.matchesSelector interact.closest = domUtils.closest interact.globalEvents = {} as any // eslint-disable-next-line no-undef interact.version = process.env.npm_package_version interact.scope = scope interact.use = function (plugin, options) { this.scope.usePlugin(plugin, options) return this } interact.isSet = function (target: Target, options?: { context?: Context }): boolean { return !!this.scope.interactables.get(target, options && options.context) } interact.on = warnOnce(function on(type: string | EventTypes, listener: ListenersArg, options?: object) { if (is.string(type) && type.search(' ') !== -1) { type = type.trim().split(/ +/) } if (is.array(type)) { for (const eventType of type as any[]) { this.on(eventType, listener, options) } return this } if (is.object(type)) { for (const prop in type) { this.on(prop, (type as any)[prop], listener) } return this } // if it is an InteractEvent type, add listener to globalEvents if (isNonNativeEvent(type, this.scope.actions)) { // if this type of event was never bound if (!this.globalEvents[type]) { this.globalEvents[type] = [listener] } else { this.globalEvents[type].push(listener) } } // If non InteractEvent type, addEventListener to document else { this.scope.events.add(this.scope.document, type, listener as Listener, { options }) } return this }, 'The interact.on() method is being deprecated') interact.off = warnOnce(function off(type: EventTypes, listener: any, options?: object) { if (is.string(type) && type.search(' ') !== -1) { type = type.trim().split(/ +/) } if (is.array(type)) { for (const eventType of type) { this.off(eventType, listener, options) } return this } if (is.object(type)) { for (const prop in type) { this.off(prop, type[prop], listener) } return this } if (isNonNativeEvent(type, this.scope.actions)) { let index: number if (type in this.globalEvents && (index = this.globalEvents[type].indexOf(listener)) !== -1) { this.globalEvents[type].splice(index, 1) } } else { this.scope.events.remove(this.scope.document, type, listener, options) } return this }, 'The interact.off() method is being deprecated') interact.debug = function () { return this.scope } interact.supportsTouch = function () { return browser.supportsTouch } interact.supportsPointerEvent = function () { return browser.supportsPointerEvent } interact.stop = function () { for (const interaction of this.scope.interactions.list) { interaction.stop() } return this } interact.pointerMoveTolerance = function (newValue?: number) { if (is.number(newValue)) { this.scope.interactions.pointerMoveTolerance = newValue return this } return this.scope.interactions.pointerMoveTolerance } interact.addDocument = function (doc: Document, options?: object) { this.scope.addDocument(doc, options) } interact.removeDocument = function (doc: Document) { this.scope.removeDocument(doc) } return interact } ================================================ FILE: packages/@interactjs/core/Interactable.spec.ts ================================================ import drag from '@interactjs/actions/drag/plugin' import * as helpers from './tests/_helpers' describe('core/Interactable', () => { test('Interactable copies and extends defaults', () => { const scope = helpers.mockScope() as any const { defaults } = scope scope.actions.methodDict = { test: 'testize' } scope.Interactable.prototype.testize = function (options: any) { this.setPerAction('test', options) } defaults.actions.test = { fromDefault: { a: 1, b: 2 }, specified: { c: 1, d: 2 }, } const specified = { specified: 'parent' } const div = scope.document.createElement('div') const interactable = scope.interactables.new(div, { test: specified }) // specified options are properly set expect(interactable.options.test.specified).toEqual(specified.specified) // default options are properly set expect(interactable.options.test.fromDefault).toEqual(defaults.actions.test.fromDefault) // defaults are not aliased expect(interactable.options.test.fromDefault).not.toBe(defaults.actions.test.fromDefault) defaults.actions.test.fromDefault.c = 3 // modifying defaults does not affect constructed interactables expect('c' in interactable.options.test.fromDefault).not.toBe(true) div.remove() }) test('Interactable unset correctly', () => { const scope = helpers.mockScope() const div = scope.document.createElement('div') const interactable = scope.interactables.new(div) expect(div[scope.id]).toHaveLength(1) interactable.unset() // clears target mapping expect(div[scope.id]).toHaveLength(0) div.remove() }) test('Interactable copies and extends per action defaults', () => { const scope = helpers.mockScope() const { defaults } = scope scope.actions.methodDict = { test: 'testize' } as any ;(scope.Interactable.prototype as any).testize = function (options: any) { this.setPerAction('test', options) } ;(defaults.perAction as any).testOption = { fromDefault: { a: 1, b: 2 }, specified: null, } ;(defaults.actions as any).test = { testOption: (defaults.perAction as any).testOption } const div = scope.document.createElement('div') const interactable = scope.interactables.new(div, {}) ;(interactable as any).testize({ testOption: { specified: 'parent' } }) // specified options are properly set expect((interactable.options as any).test).toEqual({ enabled: false, origin: { x: 0, y: 0 }, testOption: { fromDefault: { a: 1, b: 2 }, specified: 'parent', }, }) // default options are properly set expect((interactable.options as any).test.testOption.fromDefault).toEqual( (defaults.perAction as any).testOption.fromDefault, ) // defaults are not aliased expect((interactable.options as any).test.testOption.fromDefault).not.toBe( (defaults.perAction as any).testOption.fromDefault, ) ;(defaults.perAction as any).testOption.fromDefault.c = 3 // modifying defaults does not affect constructed interactables expect('c' in (interactable.options as any).test.testOption.fromDefault).toBe(false) div.remove() }) test('Interactable.updatePerActionListeners', () => { const scope = helpers.mockScope() let fired: any[] = [] function addToFired(event: any) { fired.push(event) } scope.actions.methodDict = { test: 'testize' } as any ;(scope.Interactable.prototype as any).testize = function (options: any) { this.setPerAction('test', options) } ;(scope.defaults.actions as any).test = {} const interactable = scope.interactables.new('target') interactable.setPerAction('test' as any, { listeners: [ { start: addToFired, move: addToFired, end: addToFired, }, ], }) interactable.fire({ type: 'teststart' }) expect(fired.map((e) => e.type)).toEqual(['teststart']) interactable.fire({ type: 'testmove' }) expect(fired.map((e) => e.type)).toEqual(['teststart', 'testmove']) interactable.fire({ type: 'testnotadded' }) expect(fired.map((e) => e.type)).toEqual(['teststart', 'testmove']) interactable.fire({ type: 'testend' }) expect(fired.map((e) => e.type)).toEqual(['teststart', 'testmove', 'testend']) fired = [] interactable.setPerAction('test' as any, { listeners: [{ start: addToFired }], }) interactable.fire({ type: 'teststart' }) interactable.fire({ type: 'testmove' }) interactable.fire({ type: 'testend' }) expect(fired.map((e) => e.type)).toEqual(['teststart']) fired = [] interactable.setPerAction('test' as any, { listeners: null, }) interactable.fire({ type: 'teststart' }) interactable.fire({ type: 'testmove' }) interactable.fire({ type: 'testend' }) expect(fired).toEqual([]) }) test('Interactable.{on,off}', () => { const { interactable: elInteractable, interact, target: element } = helpers.testEnv({ plugins: [drag] }) let fired: Array<{ type: any }> = [] const listener = (e: { type: any }) => fired.push(e) const selectorInteractable = interact('html') elInteractable.on('dragstart click', listener) selectorInteractable.on('dragstart click change', listener) elInteractable.fire({ type: 'dragstart' }) expect(fired).toHaveLength(1) expect(fired[0].type).toBe('dragstart') elInteractable.off('dragstart', listener) fired = [] elInteractable.fire({ type: 'dragstart' }) expect(fired).toEqual([]) element.click() expect(fired.map((e) => e.type)).toEqual(['click', 'click']) selectorInteractable.off('click', listener) fired = [] element.click() expect(fired.map((e) => e.type)).toEqual(['click']) fired = [] selectorInteractable.fire({ type: 'dragstart' }) expect(fired.map((e) => e.type)).toEqual(['dragstart']) selectorInteractable.off('dragstart', listener) fired = [] selectorInteractable.fire({ type: 'dragstart' }) expect(fired).toEqual([]) }) }) ================================================ FILE: packages/@interactjs/core/Interactable.ts ================================================ /* eslint-disable no-dupe-class-members */ import * as arr from '@interactjs/utils/arr' import browser from '@interactjs/utils/browser' import clone from '@interactjs/utils/clone' import { getElementRect, matchesUpTo, nodeContains, trySelector } from '@interactjs/utils/domUtils' import extend from '@interactjs/utils/extend' import is from '@interactjs/utils/is' import isNonNativeEvent from '@interactjs/utils/isNonNativeEvent' import normalizeListeners from '@interactjs/utils/normalizeListeners' import { getWindow } from '@interactjs/utils/window' import type { Scope } from '@interactjs/core/scope' import type { ActionMap, ActionMethod, ActionName, Actions, Context, Element, EventTypes, Listeners, ListenersArg, OrBoolean, Target, } from '@interactjs/core/types' import { Eventable } from './Eventable' import type { ActionDefaults, Defaults, OptionsArg, PerActionDefaults, Options } from './options' type IgnoreValue = string | Element | boolean type DeltaSource = 'page' | 'client' const enum OnOffMethod { On, Off, } /** * ```ts * const interactable = interact('.cards') * .draggable({ * listeners: { move: event => console.log(event.type, event.pageX, event.pageY) } * }) * .resizable({ * listeners: { move: event => console.log(event.rect) }, * modifiers: [interact.modifiers.restrictEdges({ outer: 'parent' })] * }) * ``` */ export class Interactable implements Partial { /** @internal */ get _defaults(): Defaults { return { base: {}, perAction: {}, actions: {} as ActionDefaults, } } readonly target: Target /** @internal */ readonly options!: Required /** @internal */ readonly _actions: Actions /** @internal */ readonly events = new Eventable() /** @internal */ readonly _context: Context /** @internal */ readonly _win: Window /** @internal */ readonly _doc: Document /** @internal */ readonly _scopeEvents: Scope['events'] constructor( target: Target, options: any, defaultContext: Document | Element, scopeEvents: Scope['events'], ) { this._actions = options.actions this.target = target this._context = options.context || defaultContext this._win = getWindow(trySelector(target) ? this._context : target) this._doc = this._win.document this._scopeEvents = scopeEvents this.set(options) } setOnEvents(actionName: ActionName, phases: NonNullable) { if (is.func(phases.onstart)) { this.on(`${actionName}start`, phases.onstart) } if (is.func(phases.onmove)) { this.on(`${actionName}move`, phases.onmove) } if (is.func(phases.onend)) { this.on(`${actionName}end`, phases.onend) } if (is.func(phases.oninertiastart)) { this.on(`${actionName}inertiastart`, phases.oninertiastart) } return this } updatePerActionListeners(actionName: ActionName, prev: Listeners | undefined, cur: Listeners | undefined) { const actionFilter = (this._actions.map[actionName] as { filterEventType?: (type: string) => boolean }) ?.filterEventType const filter = (type: string) => (actionFilter == null || actionFilter(type)) && isNonNativeEvent(type, this._actions) if (is.array(prev) || is.object(prev)) { this._onOff(OnOffMethod.Off, actionName, prev, undefined, filter) } if (is.array(cur) || is.object(cur)) { this._onOff(OnOffMethod.On, actionName, cur, undefined, filter) } } setPerAction(actionName: ActionName, options: OrBoolean) { const defaults = this._defaults // for all the default per-action options for (const optionName_ in options) { const optionName = optionName_ as keyof PerActionDefaults const actionOptions = this.options[actionName] const optionValue: any = options[optionName] // remove old event listeners and add new ones if (optionName === 'listeners') { this.updatePerActionListeners(actionName, actionOptions.listeners, optionValue as Listeners) } // if the option value is an array if (is.array(optionValue)) { ;(actionOptions[optionName] as any) = arr.from(optionValue) } // if the option value is an object else if (is.plainObject(optionValue)) { // copy the object ;(actionOptions[optionName] as any) = extend( actionOptions[optionName] || ({} as any), clone(optionValue), ) // set anabled field to true if it exists in the defaults if ( is.object(defaults.perAction[optionName]) && 'enabled' in (defaults.perAction[optionName] as any) ) { ;(actionOptions[optionName] as any).enabled = optionValue.enabled !== false } } // if the option value is a boolean and the default is an object else if (is.bool(optionValue) && is.object(defaults.perAction[optionName])) { ;(actionOptions[optionName] as any).enabled = optionValue } // if it's anything else, do a plain assignment else { ;(actionOptions[optionName] as any) = optionValue } } } /** * The default function to get an Interactables bounding rect. Can be * overridden using {@link Interactable.rectChecker}. * * @param {Element} [element] The element to measure. * @return {Rect} The object's bounding rectangle. */ getRect(element: Element) { element = element || (is.element(this.target) ? this.target : null) if (is.string(this.target)) { element = element || this._context.querySelector(this.target) } return getElementRect(element) } /** * Returns or sets the function used to calculate the interactable's * element's rectangle * * @param {function} [checker] A function which returns this Interactable's * bounding rectangle. See {@link Interactable.getRect} * @return {function | object} The checker function or this Interactable */ rectChecker(): (element: Element) => any | null rectChecker(checker: (element: Element) => any): this rectChecker(checker?: (element: Element) => any) { if (is.func(checker)) { this.getRect = (element) => { const rect = extend({}, checker.apply(this, element)) if (!(('width' in rect) as unknown)) { rect.width = rect.right - rect.left rect.height = rect.bottom - rect.top } return rect } return this } if (checker === null) { delete (this as Partial).getRect return this } return this.getRect } /** @internal */ _backCompatOption(optionName: keyof Options, newValue: any) { if (trySelector(newValue) || is.object(newValue)) { ;(this.options[optionName] as any) = newValue for (const action in this._actions.map) { ;(this.options[action as keyof ActionMap] as any)[optionName] = newValue } return this } return this.options[optionName] } /** * Gets or sets the origin of the Interactable's element. The x and y * of the origin will be subtracted from action event coordinates. * * @param {Element | object | string} [origin] An HTML or SVG Element whose * rect will be used, an object eg. { x: 0, y: 0 } or string 'parent', 'self' * or any CSS selector * * @return {object} The current origin or this Interactable */ origin(newValue: any) { return this._backCompatOption('origin', newValue) } /** * Returns or sets the mouse coordinate types used to calculate the * movement of the pointer. * * @param {string} [newValue] Use 'client' if you will be scrolling while * interacting; Use 'page' if you want autoScroll to work * @return {string | object} The current deltaSource or this Interactable */ deltaSource(): DeltaSource deltaSource(newValue: DeltaSource): this deltaSource(newValue?: DeltaSource) { if (newValue === 'page' || newValue === 'client') { this.options.deltaSource = newValue return this } return this.options.deltaSource } /** @internal */ getAllElements(): Element[] { const { target } = this if (is.string(target)) { return Array.from(this._context.querySelectorAll(target)) } if (is.func(target) && (target as any).getAllElements) { return (target as any).getAllElements() } return is.element(target) ? [target] : [] } /** * Gets the selector context Node of the Interactable. The default is * `window.document`. * * @return {Node} The context Node of this Interactable */ context() { return this._context } inContext(element: Document | Node) { return this._context === element.ownerDocument || nodeContains(this._context, element) } /** @internal */ testIgnoreAllow( this: Interactable, options: { ignoreFrom?: IgnoreValue; allowFrom?: IgnoreValue }, targetNode: Node, eventTarget: Node, ) { return ( !this.testIgnore(options.ignoreFrom, targetNode, eventTarget) && this.testAllow(options.allowFrom, targetNode, eventTarget) ) } /** @internal */ testAllow(this: Interactable, allowFrom: IgnoreValue | undefined, targetNode: Node, element: Node) { if (!allowFrom) { return true } if (!is.element(element)) { return false } if (is.string(allowFrom)) { return matchesUpTo(element, allowFrom, targetNode) } else if (is.element(allowFrom)) { return nodeContains(allowFrom, element) } return false } /** @internal */ testIgnore(this: Interactable, ignoreFrom: IgnoreValue | undefined, targetNode: Node, element: Node) { if (!ignoreFrom || !is.element(element)) { return false } if (is.string(ignoreFrom)) { return matchesUpTo(element, ignoreFrom, targetNode) } else if (is.element(ignoreFrom)) { return nodeContains(ignoreFrom, element) } return false } /** * Calls listeners for the given InteractEvent type bound globally * and directly to this Interactable * * @param {InteractEvent} iEvent The InteractEvent object to be fired on this * Interactable * @return {Interactable} this Interactable */ fire(iEvent: E) { this.events.fire(iEvent) return this } /** @internal */ _onOff( method: OnOffMethod, typeArg: EventTypes, listenerArg?: ListenersArg | null, options?: any, filter?: (type: string) => boolean, ) { if (is.object(typeArg) && !is.array(typeArg)) { options = listenerArg listenerArg = null } const listeners = normalizeListeners(typeArg, listenerArg, filter) for (let type in listeners) { if (type === 'wheel') { type = browser.wheelEvent } for (const listener of listeners[type]) { // if it is an action event type if (isNonNativeEvent(type, this._actions)) { this.events[method === OnOffMethod.On ? 'on' : 'off'](type, listener) } // delegated event else if (is.string(this.target)) { this._scopeEvents[method === OnOffMethod.On ? 'addDelegate' : 'removeDelegate']( this.target, this._context, type, listener, options, ) } // remove listener from this Interactable's element else { this._scopeEvents[method === OnOffMethod.On ? 'add' : 'remove']( this.target, type, listener, options, ) } } } return this } /** * Binds a listener for an InteractEvent, pointerEvent or DOM event. * * @param {string | array | object} types The types of events to listen * for * @param {function | array | object} [listener] The event listener function(s) * @param {object | boolean} [options] options object or useCapture flag for * addEventListener * @return {Interactable} This Interactable */ on(types: EventTypes, listener?: ListenersArg, options?: any) { return this._onOff(OnOffMethod.On, types, listener, options) } /** * Removes an InteractEvent, pointerEvent or DOM event listener. * * @param {string | array | object} types The types of events that were * listened for * @param {function | array | object} [listener] The event listener function(s) * @param {object | boolean} [options] options object or useCapture flag for * removeEventListener * @return {Interactable} This Interactable */ off(types: string | string[] | EventTypes, listener?: ListenersArg, options?: any) { return this._onOff(OnOffMethod.Off, types, listener, options) } /** * Reset the options of this Interactable * * @param {object} options The new settings to apply * @return {object} This Interactable */ set(options: OptionsArg) { const defaults = this._defaults if (!is.object(options)) { options = {} } ;(this.options as Required) = clone(defaults.base) as Required for (const actionName_ in this._actions.methodDict) { const actionName = actionName_ as ActionName const methodName = this._actions.methodDict[actionName] this.options[actionName] = {} this.setPerAction(actionName, extend(extend({}, defaults.perAction), defaults.actions[actionName])) ;(this[methodName] as ActionMethod)(options[actionName]) } for (const setting in options) { if (setting === 'getRect') { this.rectChecker(options.getRect) continue } if (is.func((this as any)[setting])) { ;(this as any)[setting](options[setting as keyof typeof options]) } } return this } /** * Remove this interactable from the list of interactables and remove it's * action capabilities and event listeners */ unset() { if (is.string(this.target)) { // remove delegated events for (const type in this._scopeEvents.delegatedEvents) { const delegated = this._scopeEvents.delegatedEvents[type] for (let i = delegated.length - 1; i >= 0; i--) { const { selector, context, listeners } = delegated[i] if (selector === this.target && context === this._context) { delegated.splice(i, 1) } for (let l = listeners.length - 1; l >= 0; l--) { this._scopeEvents.removeDelegate( this.target, this._context, type, listeners[l][0], listeners[l][1], ) } } } } else { this._scopeEvents.remove(this.target, 'all') } } } ================================================ FILE: packages/@interactjs/core/InteractableSet.ts ================================================ import * as arr from '@interactjs/utils/arr' import * as domUtils from '@interactjs/utils/domUtils' import extend from '@interactjs/utils/extend' import is from '@interactjs/utils/is' import type { Interactable } from '@interactjs/core/Interactable' import type { OptionsArg, Options } from '@interactjs/core/options' import type { Scope } from '@interactjs/core/scope' import type { Target } from '@interactjs/core/types' declare module '@interactjs/core/scope' { interface SignalArgs { 'interactable:new': { interactable: Interactable target: Target options: OptionsArg win: Window } } } export class InteractableSet { // all set interactables list: Interactable[] = [] selectorMap: { [selector: string]: Interactable[] } = {} scope: Scope constructor(scope: Scope) { this.scope = scope scope.addListeners({ 'interactable:unset': ({ interactable }) => { const { target } = interactable const interactablesOnTarget: Interactable[] = is.string(target) ? this.selectorMap[target] : (target as any)[this.scope.id] const targetIndex = arr.findIndex(interactablesOnTarget, (i) => i === interactable) interactablesOnTarget.splice(targetIndex, 1) }, }) } new(target: Target, options?: any): Interactable { options = extend(options || {}, { actions: this.scope.actions, }) const interactable = new this.scope.Interactable(target, options, this.scope.document, this.scope.events) this.scope.addDocument(interactable._doc) this.list.push(interactable) if (is.string(target)) { if (!this.selectorMap[target]) { this.selectorMap[target] = [] } this.selectorMap[target].push(interactable) } else { if (!(interactable.target as any)[this.scope.id]) { Object.defineProperty(target, this.scope.id, { value: [], configurable: true, }) } ;(target as any)[this.scope.id].push(interactable) } this.scope.fire('interactable:new', { target, options, interactable, win: this.scope._win, }) return interactable } getExisting(target: Target, options?: Options) { const context = (options && options.context) || this.scope.document const isSelector = is.string(target) const interactablesOnTarget: Interactable[] = isSelector ? this.selectorMap[target as string] : (target as any)[this.scope.id] if (!interactablesOnTarget) return undefined return arr.find( interactablesOnTarget, (interactable) => interactable._context === context && (isSelector || interactable.inContext(target as any)), ) } forEachMatch(node: Node, callback: (interactable: Interactable) => T): T | void { for (const interactable of this.list) { let ret: T if ( (is.string(interactable.target) ? // target is a selector and the element matches is.element(node) && domUtils.matchesSelector(node, interactable.target) : // target is the element node === interactable.target) && // the element is in context interactable.inContext(node) ) { ret = callback(interactable) } if (ret !== undefined) { return ret } } } } ================================================ FILE: packages/@interactjs/core/Interaction.spec.ts ================================================ import drag from '@interactjs/actions/drag/plugin' import drop from '@interactjs/actions/drop/plugin' import autoStart from '@interactjs/auto-start/base' import extend from '@interactjs/utils/extend' import * as pointerUtils from '@interactjs/utils/pointerUtils' import type { PointerType } from '@interactjs/core/types' import type { EventPhase } from './InteractEvent' import { InteractEvent } from './InteractEvent' import { Interaction } from './Interaction' import * as helpers from './tests/_helpers' describe('core/Interaction', () => { test('constructor', () => { const testType = 'test' const dummyScopeFire = () => {} const interaction = new Interaction({ pointerType: testType, scopeFire: dummyScopeFire, }) const zeroCoords = { page: { x: 0, y: 0 }, client: { x: 0, y: 0 }, timeStamp: 0, } // scopeFire option is set assigned to interaction._scopeFire expect(interaction._scopeFire).toBe(dummyScopeFire) expect(interaction.prepared).toEqual(expect.any(Object)) expect(interaction.downPointer).toEqual(expect.any(Object)) // `interaction.coords.${coordField} set to zero` expect(interaction.coords).toEqual({ start: zeroCoords, cur: zeroCoords, prev: zeroCoords, delta: zeroCoords, velocity: zeroCoords, }) // interaction.pointerType is set expect(interaction.pointerType).toBe(testType) // interaction.pointers is initially an empty array expect(interaction.pointers).toEqual([]) // false properties expect(interaction).toMatchObject({ pointerIsDown: false, pointerWasMoved: false, _interacting: false }) expect(interaction.pointerType).not.toBe('mouse') }) test('Interaction destroy', () => { const { interaction } = helpers.testEnv() const pointer = { pointerId: 10 } as any const event = {} as any interaction.updatePointer(pointer, event, null) interaction.destroy() // interaction._latestPointer.pointer is null expect(interaction._latestPointer.pointer).toBeNull() // interaction._latestPointer.event is null expect(interaction._latestPointer.event).toBeNull() // interaction._latestPointer.eventTarget is null expect(interaction._latestPointer.eventTarget).toBeNull() }) test('Interaction.getPointerIndex', () => { const { interaction } = helpers.testEnv() interaction.pointers = [2, 4, 5, 0, -1].map((id) => ({ id })) as any interaction.pointers.forEach(({ id }, index) => { expect(interaction.getPointerIndex({ pointerId: id } as any)).toBe(index) }) }) describe('Interaction.updatePointer', () => { test('no existing pointers', () => { const { interaction } = helpers.testEnv() const pointer = { pointerId: 10 } as any const event = {} as any const ret = interaction.updatePointer(pointer, event, null) // interaction.pointers == [{ pointer, ... }] expect(interaction.pointers).toEqual([ { id: pointer.pointerId, pointer, event, downTime: null, downTarget: null, }, ]) // new pointer index is returned expect(ret).toBe(0) }) test('new pointer with exisiting pointer', () => { const { interaction } = helpers.testEnv() const existing: any = { pointerId: 0 } const event: any = {} interaction.updatePointer(existing, event, null) const newPointer: any = { pointerId: 10 } const ret = interaction.updatePointer(newPointer, event, null) // interaction.pointers == [{ pointer: existing, ... }, { pointer: newPointer, ... }] expect(interaction.pointers).toEqual([ { id: existing.pointerId, pointer: existing, event, downTime: null, downTarget: null, }, { id: newPointer.pointerId, pointer: newPointer, event, downTime: null, downTarget: null, }, ]) // second pointer index is 1 expect(ret).toBe(1) }) test('update existing pointers', () => { const { interaction } = helpers.testEnv() const oldPointers = [-3, 10, 2].map((pointerId) => ({ pointerId })) const newPointers = oldPointers.map((pointer) => ({ ...pointer, new: true })) oldPointers.forEach((pointer: any) => interaction.updatePointer(pointer, pointer, null)) newPointers.forEach((pointer: any) => interaction.updatePointer(pointer, pointer, null)) // number of pointers is unchanged expect(interaction.pointers).toHaveLength(oldPointers.length) interaction.pointers.forEach((pointerInfo, i) => { // `pointer[${i}].id is the same` expect(pointerInfo.id).toBe(oldPointers[i].pointerId) // `new pointer ${i} !== old pointer object` expect(pointerInfo.pointer).not.toBe(oldPointers[i]) }) }) }) test('Interaction.removePointer', () => { const { interaction } = helpers.testEnv() const ids = [0, 1, 2, 3] const removals = [ { id: 0, remain: [1, 2, 3], message: 'first of 4' }, { id: 2, remain: [1, 3], message: 'middle of 3' }, { id: 3, remain: [1], message: 'last of 2' }, { id: 1, remain: [], message: 'final' }, ] ids.forEach((pointerId) => interaction.updatePointer({ pointerId } as any, {} as any, null)) for (const removal of removals) { interaction.removePointer({ pointerId: removal.id } as PointerType, null) // `${removal.message} - remaining interaction.pointers is correct` expect(interaction.pointers.map((p) => p.id)).toEqual(removal.remain) } }) test('Interaction.pointer{Down,Move,Up} updatePointer', () => { const { scope, interaction } = helpers.testEnv() const eventTarget: any = {} const pointer: any = { target: eventTarget, pointerId: 0, } let info: any = {} scope.addListeners({ 'interactions:update-pointer': (arg) => { info.updated = arg.pointerInfo }, 'interactions:remove-pointer': (arg) => { info.removed = arg.pointerInfo }, }) interaction.coords.cur.timeStamp = 0 const commonPointerInfo: any = { id: 0, pointer, event: pointer, downTime: null, downTarget: null, } interaction.pointerDown(pointer, pointer, eventTarget) // interaction.pointerDown updates pointer expect(info.updated).toEqual({ ...commonPointerInfo, downTime: interaction.coords.cur.timeStamp, downTarget: eventTarget, }) // interaction.pointerDown doesn't remove pointer expect(info.removed).toBeUndefined() interaction.removePointer(pointer, null) info = {} interaction.pointerMove(pointer, pointer, eventTarget) // interaction.pointerMove updates pointer expect(info.updated).toEqual(commonPointerInfo) // interaction.pointerMove doesn't remove pointer expect(info.removed).toBeUndefined() info = {} interaction.pointerUp(pointer, pointer, eventTarget, null) // interaction.pointerUp doesn't update existing pointer expect(info.updated).toBeUndefined() info = {} interaction.pointerUp(pointer, pointer, eventTarget, null) // interaction.pointerUp updates non existing pointer expect(info.updated).toEqual(commonPointerInfo) // interaction.pointerUp also removes pointer expect(info.removed).toEqual(commonPointerInfo) info = {} }) test('Interaction.pointerDown', () => { const { interaction, scope, coords, event, target } = helpers.testEnv() let signalArg: any const coordsSet = helpers.newCoordsSet() scope.now = () => coords.timeStamp extend(coords, { target, type: 'down', }) const signalListener = (arg: any) => { signalArg = arg } scope.addListeners({ 'interactions:down': signalListener, }) const pointerCoords: any = { page: {}, client: {} } pointerUtils.setCoords(pointerCoords, [event], event.timeStamp) for (const prop in coordsSet) { pointerUtils.copyCoords( interaction.coords[prop as keyof typeof coordsSet], coordsSet[prop as keyof typeof coordsSet], ) } // downPointer is initially empty expect(interaction.downPointer).toEqual({} as any) // test while interacting interaction._interacting = true interaction.pointerDown(event, event, target) // downEvent is not updated expect(interaction.downEvent).toBeNull() // pointer is added expect(interaction.pointers).toEqual([ { id: event.pointerId, event, pointer: event, downTime: 0, downTarget: target, }, ]) // downPointer is updated expect(interaction.downPointer).not.toEqual({} as any) // coords.start are not modified expect(interaction.coords.start).toEqual(coordsSet.start) // coords.prev are not modified expect(interaction.coords.prev).toEqual(coordsSet.prev) // coords.cur *are* modified expect(interaction.coords.cur).toEqual(helpers.getProps(event, ['page', 'client', 'timeStamp'])) // pointerIsDown expect(interaction.pointerIsDown).toBe(true) // !pointerWasMoved expect(interaction.pointerWasMoved).toBe(false) // pointer in down signal arg expect(signalArg.pointer).toBe(event) // event in down signal arg expect(signalArg.event).toBe(event) // eventTarget in down signal arg expect(signalArg.eventTarget).toBe(target) // pointerIndex in down signal arg expect(signalArg.pointerIndex).toBe(0) // test while not interacting interaction._interacting = false // reset pointerIsDown interaction.pointerIsDown = false // pretend pointer was moved interaction.pointerWasMoved = true // reset signalArg object signalArg = undefined interaction.removePointer(event, null) interaction.pointerDown(event, event, target) // timeStamp is assigned with new Date.getTime() // don't let it cause deepEaual to fail pointerCoords.timeStamp = interaction.coords.start.timeStamp // downEvent is updated expect(interaction.downEvent).toBe(event) // interaction.pointers is updated expect(interaction.pointers).toEqual([ { id: event.pointerId, event, pointer: event, downTime: pointerCoords.timeStamp, downTarget: target, }, ]) // coords.start are set to pointer expect(interaction.coords.start).toEqual(pointerCoords) // coords.cur are set to pointer expect(interaction.coords.cur).toEqual(pointerCoords) // coords.prev are set to pointer expect(interaction.coords.prev).toEqual(pointerCoords) // down signal was fired again expect(signalArg).toBeInstanceOf(Object) // pointerIsDown expect(interaction.pointerIsDown).toBe(true) // pointerWasMoved should always change to false expect(interaction.pointerWasMoved).toBe(false) }) test('Interaction.start', () => { const { interaction, interactable, scope, event, target: element, down, stop, } = helpers.testEnv({ plugins: [drag], }) const action = { name: 'drag' } as const interaction.start(action, interactable, element) // do nothing if !pointerIsDown expect(interaction.prepared.name).toBeNull() // pointers is still empty interaction.pointerIsDown = true interaction.start(action, interactable, element) // do nothing if too few pointers are down expect(interaction.prepared.name).toBeNull() down() interaction._interacting = true interaction.start(action, interactable, element) // do nothing if already interacting expect(interaction.prepared.name).toBeNull() interaction._interacting = false interactable.options[action.name] = { enabled: false } interaction.start(action, interactable, element) // do nothing if action is not enabled expect(interaction.prepared.name).toBeNull() interactable.options[action.name] = { enabled: true } let signalArg: any // let interactingInStartListener const signalListener = (arg: any) => { signalArg = arg // interactingInStartListener = arg.interaction.interacting() } scope.addListeners({ 'interactions:action-start': signalListener, }) interaction.start(action, interactable, element) // action is prepared expect(interaction.prepared.name).toBe(action.name) // interaction.interactable is updated expect(interaction.interactable).toBe(interactable) // interaction.element is updated expect(interaction.element).toBe(element) // t.assert(interactingInStartListener, 'interaction is interacting during action-start signal') // interaction is interacting after start method expect(interaction.interacting()).toBe(true) // interaction in signal arg expect(signalArg.interaction).toBe(interaction) // event (interaction.downEvent) in signal arg expect(signalArg.event).toBe(event) stop() }) test('interaction move() and stop() from start event', () => { const { interaction, interactable, target, down } = helpers.testEnv({ plugins: [drag, drop, autoStart] }) let stoppedBeforeStartFired: boolean interactable.draggable({ listeners: { start(event) { stoppedBeforeStartFired = interaction._stopped // interaction.move() doesn't throw from start event expect(() => event.interaction.move()).not.toThrow() // interaction.stop() doesn't throw from start event expect(() => event.interaction.stop()).not.toThrow() }, }, }) down() interaction.start({ name: 'drag' }, interactable, target as HTMLElement) // !interaction._stopped in start listener expect(stoppedBeforeStartFired).toBe(false) // interaction can be stopped from start event listener expect(interaction.interacting()).toBe(false) // interaction._stopped after stop() in start listener expect(interaction._stopped).toBe(true) }) test('Interaction createPreparedEvent', () => { const { interaction, interactable, target } = helpers.testEnv() const action = { name: 'resize' } as const const phase = 'TEST_PHASE' as EventPhase interaction.prepared = action interaction.interactable = interactable interaction.element = target interaction.prevEvent = { page: {}, client: {}, velocity: {} } as any const iEvent = interaction._createPreparedEvent({} as any, phase) expect(iEvent).toBeInstanceOf(InteractEvent) expect(iEvent.type).toBe(action.name + phase) expect(iEvent.interactable).toBe(interactable) expect(iEvent.target).toBe(interactable.target) }) test('Interaction fireEvent', () => { const { interaction, interactable } = helpers.testEnv() const iEvent = {} as InteractEvent // this method should be called from actions.firePrepared interactable.fire = jest.fn() interaction.interactable = interactable interaction._fireEvent(iEvent) // target interactable's fire method is called expect(interactable.fire).toHaveBeenCalledWith(iEvent) // interaction.prevEvent is updated expect(interaction.prevEvent).toBe(iEvent) }) }) ================================================ FILE: packages/@interactjs/core/Interaction.ts ================================================ import * as arr from '@interactjs/utils/arr' import extend from '@interactjs/utils/extend' import hypot from '@interactjs/utils/hypot' import { warnOnce, copyAction } from '@interactjs/utils/misc' import * as pointerUtils from '@interactjs/utils/pointerUtils' import * as rectUtils from '@interactjs/utils/rect' import type { Element, EdgeOptions, PointerEventType, PointerType, FullRect, CoordsSet, ActionName, ActionProps, } from '@interactjs/core/types' import type { Interactable } from './Interactable' import type { EventPhase } from './InteractEvent' import { InteractEvent } from './InteractEvent' import type { ActionDefaults } from './options' import { PointerInfo } from './PointerInfo' import type { Scope } from './scope' export enum _ProxyValues { interactable = '', element = '', prepared = '', pointerIsDown = '', pointerWasMoved = '', _proxy = '', } export enum _ProxyMethods { start = '', move = '', end = '', stop = '', interacting = '', } export type PointerArgProps = { pointer: PointerType event: PointerEventType eventTarget: Node pointerIndex: number pointerInfo: PointerInfo interaction: Interaction } & T export interface DoPhaseArg { event: PointerEventType phase: EventPhase interaction: Interaction iEvent: InteractEvent preEnd?: boolean type?: string } export type DoAnyPhaseArg = DoPhaseArg declare module '@interactjs/core/scope' { interface SignalArgs { 'interactions:new': { interaction: Interaction } 'interactions:down': PointerArgProps<{ type: 'down' }> 'interactions:move': PointerArgProps<{ type: 'move' dx: number dy: number duplicate: boolean }> 'interactions:up': PointerArgProps<{ type: 'up' curEventTarget: EventTarget }> 'interactions:cancel': SignalArgs['interactions:up'] & { type: 'cancel' curEventTarget: EventTarget } 'interactions:update-pointer': PointerArgProps<{ down: boolean }> 'interactions:remove-pointer': PointerArgProps 'interactions:blur': { interaction: Interaction; event: Event; type: 'blur' } 'interactions:before-action-start': Omit 'interactions:action-start': DoAnyPhaseArg 'interactions:after-action-start': DoAnyPhaseArg 'interactions:before-action-move': Omit 'interactions:action-move': DoAnyPhaseArg 'interactions:after-action-move': DoAnyPhaseArg 'interactions:before-action-end': Omit 'interactions:action-end': DoAnyPhaseArg 'interactions:after-action-end': DoAnyPhaseArg 'interactions:stop': { interaction: Interaction } } } export type InteractionProxy = Pick< Interaction, Exclude > let idCounter = 0 export class Interaction { /** current interactable being interacted with */ interactable: Interactable | null = null /** the target element of the interactable */ element: Element | null = null rect: FullRect | null = null /** @internal */ _rects?: { start: FullRect corrected: FullRect previous: FullRect delta: FullRect } /** @internal */ edges: EdgeOptions | null = null /** @internal */ _scopeFire: Scope['fire'] // action that's ready to be fired on next move event prepared: ActionProps = { name: null, axis: null, edges: null, } pointerType: string /** @internal keep track of added pointers */ pointers: PointerInfo[] = [] /** @internal pointerdown/mousedown/touchstart event */ downEvent: PointerEventType | null = null /** @internal */ downPointer: PointerType = {} as PointerType /** @internal */ _latestPointer: { pointer: PointerType event: PointerEventType eventTarget: Node } = { pointer: null, event: null, eventTarget: null, } /** @internal */ prevEvent: InteractEvent = null pointerIsDown = false pointerWasMoved = false /** @internal */ _interacting = false /** @internal */ _ending = false /** @internal */ _stopped = true /** @internal */ _proxy: InteractionProxy /** @internal */ simulation = null /** @internal */ get pointerMoveTolerance() { return 1 } doMove = warnOnce(function (this: Interaction, signalArg: any) { this.move(signalArg) }, 'The interaction.doMove() method has been renamed to interaction.move()') coords: CoordsSet = { // Starting InteractEvent pointer coordinates start: pointerUtils.newCoords(), // Previous native pointer move event coordinates prev: pointerUtils.newCoords(), // current native pointer move event coordinates cur: pointerUtils.newCoords(), // Change in coordinates and time of the pointer delta: pointerUtils.newCoords(), // pointer velocity velocity: pointerUtils.newCoords(), } /** @internal */ readonly _id: number = idCounter++ constructor({ pointerType, scopeFire }: { pointerType?: string; scopeFire: Scope['fire'] }) { this._scopeFire = scopeFire this.pointerType = pointerType const that = this this._proxy = {} as InteractionProxy for (const key in _ProxyValues) { Object.defineProperty(this._proxy, key, { get() { return that[key] }, }) } for (const key in _ProxyMethods) { Object.defineProperty(this._proxy, key, { value: (...args: any[]) => that[key](...args), }) } this._scopeFire('interactions:new', { interaction: this }) } pointerDown(pointer: PointerType, event: PointerEventType, eventTarget: Node) { const pointerIndex = this.updatePointer(pointer, event, eventTarget, true) const pointerInfo = this.pointers[pointerIndex] this._scopeFire('interactions:down', { pointer, event, eventTarget, pointerIndex, pointerInfo, type: 'down', interaction: this as unknown as Interaction, }) } /** * ```js * interact(target) * .draggable({ * // disable the default drag start by down->move * manualStart: true * }) * // start dragging after the user holds the pointer down * .on('hold', function (event) { * var interaction = event.interaction * * if (!interaction.interacting()) { * interaction.start({ name: 'drag' }, * event.interactable, * event.currentTarget) * } * }) * ``` * * Start an action with the given Interactable and Element as tartgets. The * action must be enabled for the target Interactable and an appropriate * number of pointers must be held down - 1 for drag/resize, 2 for gesture. * * Use it with `interactable.able({ manualStart: false })` to always * [start actions manually](https://github.com/taye/interact.js/issues/114) * * @param action - The action to be performed - drag, resize, etc. * @param target - The Interactable to target * @param element - The DOM Element to target * @returns Whether the interaction was successfully started */ start(action: ActionProps, interactable: Interactable, element: Element): boolean { if ( this.interacting() || !this.pointerIsDown || this.pointers.length < (action.name === 'gesture' ? 2 : 1) || !interactable.options[action.name as keyof ActionDefaults].enabled ) { return false } copyAction(this.prepared, action) this.interactable = interactable this.element = element this.rect = interactable.getRect(element) this.edges = this.prepared.edges ? extend({}, this.prepared.edges) : { left: true, right: true, top: true, bottom: true } this._stopped = false this._interacting = this._doPhase({ interaction: this, event: this.downEvent, phase: 'start', }) && !this._stopped return this._interacting } pointerMove(pointer: PointerType, event: PointerEventType, eventTarget: Node) { if (!this.simulation && !(this.modification && this.modification.endResult)) { this.updatePointer(pointer, event, eventTarget, false) } const duplicateMove = this.coords.cur.page.x === this.coords.prev.page.x && this.coords.cur.page.y === this.coords.prev.page.y && this.coords.cur.client.x === this.coords.prev.client.x && this.coords.cur.client.y === this.coords.prev.client.y let dx: number let dy: number // register movement greater than pointerMoveTolerance if (this.pointerIsDown && !this.pointerWasMoved) { dx = this.coords.cur.client.x - this.coords.start.client.x dy = this.coords.cur.client.y - this.coords.start.client.y this.pointerWasMoved = hypot(dx, dy) > this.pointerMoveTolerance } const pointerIndex = this.getPointerIndex(pointer) const signalArg = { pointer, pointerIndex, pointerInfo: this.pointers[pointerIndex], event, type: 'move' as const, eventTarget, dx, dy, duplicate: duplicateMove, interaction: this as unknown as Interaction, } if (!duplicateMove) { // set pointer coordinate, time changes and velocity pointerUtils.setCoordVelocity(this.coords.velocity, this.coords.delta) } this._scopeFire('interactions:move', signalArg) if (!duplicateMove && !this.simulation) { // if interacting, fire an 'action-move' signal etc if (this.interacting()) { signalArg.type = null this.move(signalArg) } if (this.pointerWasMoved) { pointerUtils.copyCoords(this.coords.prev, this.coords.cur) } } } /** * ```js * interact(target) * .draggable(true) * .on('dragmove', function (event) { * if (someCondition) { * // change the snap settings * event.interactable.draggable({ snap: { targets: [] }}) * // fire another move event with re-calculated snap * event.interaction.move() * } * }) * ``` * * Force a move of the current action at the same coordinates. Useful if * snap/restrict has been changed and you want a movement with the new * settings. */ move(signalArg?: any) { if (!signalArg || !signalArg.event) { pointerUtils.setZeroCoords(this.coords.delta) } signalArg = extend( { pointer: this._latestPointer.pointer, event: this._latestPointer.event, eventTarget: this._latestPointer.eventTarget, interaction: this, }, signalArg || {}, ) signalArg.phase = 'move' this._doPhase(signalArg) } /** * @internal * End interact move events and stop auto-scroll unless simulation is running */ pointerUp(pointer: PointerType, event: PointerEventType, eventTarget: Node, curEventTarget: EventTarget) { let pointerIndex = this.getPointerIndex(pointer) if (pointerIndex === -1) { pointerIndex = this.updatePointer(pointer, event, eventTarget, false) } const type = /cancel$/i.test(event.type) ? 'cancel' : 'up' this._scopeFire(`interactions:${type}` as 'interactions:up' | 'interactions:cancel', { pointer, pointerIndex, pointerInfo: this.pointers[pointerIndex], event, eventTarget, type: type as any, curEventTarget, interaction: this as unknown as Interaction, }) if (!this.simulation) { this.end(event) } this.removePointer(pointer, event) } /** @internal */ documentBlur(event: Event) { this.end(event as any) this._scopeFire('interactions:blur', { event, type: 'blur', interaction: this as unknown as Interaction, }) } /** * ```js * interact(target) * .draggable(true) * .on('move', function (event) { * if (event.pageX > 1000) { * // end the current action * event.interaction.end() * // stop all further listeners from being called * event.stopImmediatePropagation() * } * }) * ``` */ end(event?: PointerEventType) { this._ending = true event = event || this._latestPointer.event let endPhaseResult: boolean if (this.interacting()) { endPhaseResult = this._doPhase({ event, interaction: this, phase: 'end', }) } this._ending = false if (endPhaseResult === true) { this.stop() } } currentAction() { return this._interacting ? this.prepared.name : null } interacting() { return this._interacting } stop() { this._scopeFire('interactions:stop', { interaction: this }) this.interactable = this.element = null this._interacting = false this._stopped = true this.prepared.name = this.prevEvent = null } /** @internal */ getPointerIndex(pointer: any) { const pointerId = pointerUtils.getPointerId(pointer) // mouse and pen interactions may have only one pointer return this.pointerType === 'mouse' || this.pointerType === 'pen' ? this.pointers.length - 1 : arr.findIndex(this.pointers, (curPointer) => curPointer.id === pointerId) } /** @internal */ getPointerInfo(pointer: any) { return this.pointers[this.getPointerIndex(pointer)] } /** @internal */ updatePointer(pointer: PointerType, event: PointerEventType, eventTarget: Node, down?: boolean) { const id = pointerUtils.getPointerId(pointer) let pointerIndex = this.getPointerIndex(pointer) let pointerInfo = this.pointers[pointerIndex] down = down === false ? false : down || /(down|start)$/i.test(event.type) if (!pointerInfo) { pointerInfo = new PointerInfo(id, pointer, event, null, null) pointerIndex = this.pointers.length this.pointers.push(pointerInfo) } else { pointerInfo.pointer = pointer } pointerUtils.setCoords( this.coords.cur, this.pointers.map((p) => p.pointer), this._now(), ) pointerUtils.setCoordDeltas(this.coords.delta, this.coords.prev, this.coords.cur) if (down) { this.pointerIsDown = true pointerInfo.downTime = this.coords.cur.timeStamp pointerInfo.downTarget = eventTarget pointerUtils.pointerExtend(this.downPointer, pointer) if (!this.interacting()) { pointerUtils.copyCoords(this.coords.start, this.coords.cur) pointerUtils.copyCoords(this.coords.prev, this.coords.cur) this.downEvent = event this.pointerWasMoved = false } } this._updateLatestPointer(pointer, event, eventTarget) this._scopeFire('interactions:update-pointer', { pointer, event, eventTarget, down, pointerInfo, pointerIndex, interaction: this as unknown as Interaction, }) return pointerIndex } /** @internal */ removePointer(pointer: PointerType, event: PointerEventType) { const pointerIndex = this.getPointerIndex(pointer) if (pointerIndex === -1) return const pointerInfo = this.pointers[pointerIndex] this._scopeFire('interactions:remove-pointer', { pointer, event, eventTarget: null, pointerIndex, pointerInfo, interaction: this as unknown as Interaction, }) this.pointers.splice(pointerIndex, 1) this.pointerIsDown = false } /** @internal */ _updateLatestPointer(pointer: PointerType, event: PointerEventType, eventTarget: Node) { this._latestPointer.pointer = pointer this._latestPointer.event = event this._latestPointer.eventTarget = eventTarget } destroy() { this._latestPointer.pointer = null this._latestPointer.event = null this._latestPointer.eventTarget = null } /** @internal */ _createPreparedEvent

( event: PointerEventType, phase: P, preEnd?: boolean, type?: string, ) { return new InteractEvent(this, event, this.prepared.name, phase, this.element, preEnd, type) } /** @internal */ _fireEvent

(iEvent: InteractEvent) { this.interactable?.fire(iEvent) if (!this.prevEvent || iEvent.timeStamp >= this.prevEvent.timeStamp) { this.prevEvent = iEvent } } /** @internal */ _doPhase

( signalArg: Omit, 'iEvent'> & { iEvent?: InteractEvent }, ) { const { event, phase, preEnd, type } = signalArg const { rect } = this if (rect && phase === 'move') { // update the rect changes due to pointer move rectUtils.addEdges(this.edges, rect, this.coords.delta[this.interactable.options.deltaSource]) rect.width = rect.right - rect.left rect.height = rect.bottom - rect.top } const beforeResult = this._scopeFire(`interactions:before-action-${phase}` as any, signalArg) if (beforeResult === false) { return false } const iEvent = (signalArg.iEvent = this._createPreparedEvent(event, phase, preEnd, type)) this._scopeFire(`interactions:action-${phase}` as any, signalArg) if (phase === 'start') { this.prevEvent = iEvent } this._fireEvent(iEvent) this._scopeFire(`interactions:after-action-${phase}` as any, signalArg) return true } /** @internal */ _now() { return Date.now() } } export default Interaction export { PointerInfo } ================================================ FILE: packages/@interactjs/core/NativeTypes.ts ================================================ export const NativePointerEvent = null as unknown as InstanceType export type NativeEventTarget = EventTarget export type NativeElement = Element ================================================ FILE: packages/@interactjs/core/PointerInfo.ts ================================================ import type { PointerEventType, PointerType } from '@interactjs/core/types' export class PointerInfo { id: number pointer: PointerType event: PointerEventType downTime: number downTarget: Node constructor(id: number, pointer: PointerType, event: PointerEventType, downTime: number, downTarget: Node) { this.id = id this.pointer = pointer this.event = event this.downTime = downTime this.downTarget = downTarget } } ================================================ FILE: packages/@interactjs/core/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/core/events.ts ================================================ import * as arr from '@interactjs/utils/arr' import * as domUtils from '@interactjs/utils/domUtils' import is from '@interactjs/utils/is' import pExtend from '@interactjs/utils/pointerExtend' import * as pointerUtils from '@interactjs/utils/pointerUtils' import type { Scope } from '@interactjs/core/scope' import type { Element } from '@interactjs/core/types' import type { NativeEventTarget } from './NativeTypes' declare module '@interactjs/core/scope' { interface Scope { events: ReturnType } } interface EventOptions { capture: boolean passive: boolean } type PartialEventTarget = Partial type ListenerEntry = { func: (event: Event | FakeEvent) => any; options: EventOptions } function install(scope: Scope) { const targets: Array<{ eventTarget: PartialEventTarget events: { [type: string]: ListenerEntry[] } }> = [] const delegatedEvents: { [type: string]: Array<{ selector: string context: Node listeners: ListenerEntry[] }> } = {} const documents: Document[] = [] const eventsMethods = { add, remove, addDelegate, removeDelegate, delegateListener, delegateUseCapture, delegatedEvents, documents, targets, supportsOptions: false, supportsPassive: false, } // check if browser supports passive events and options arg scope.document?.createElement('div').addEventListener('test', null, { get capture() { return (eventsMethods.supportsOptions = true) }, get passive() { return (eventsMethods.supportsPassive = true) }, }) scope.events = eventsMethods function add( eventTarget: PartialEventTarget, type: string, listener: ListenerEntry['func'], optionalArg?: boolean | EventOptions, ) { if (!eventTarget.addEventListener) return const options = getOptions(optionalArg) let target = arr.find(targets, (t) => t.eventTarget === eventTarget) if (!target) { target = { eventTarget, events: {}, } targets.push(target) } if (!target.events[type]) { target.events[type] = [] } if (!arr.find(target.events[type], (l) => l.func === listener && optionsMatch(l.options, options))) { eventTarget.addEventListener( type, listener as any, eventsMethods.supportsOptions ? options : options.capture, ) target.events[type].push({ func: listener, options }) } } function remove( eventTarget: PartialEventTarget, type: string, listener?: 'all' | ListenerEntry['func'], optionalArg?: boolean | EventOptions, ) { if (!eventTarget.addEventListener || !eventTarget.removeEventListener) return const targetIndex = arr.findIndex(targets, (t) => t.eventTarget === eventTarget) const target = targets[targetIndex] if (!target || !target.events) { return } if (type === 'all') { for (type in target.events) { if (target.events.hasOwnProperty(type)) { remove(eventTarget, type, 'all') } } return } let typeIsEmpty = false const typeListeners = target.events[type] if (typeListeners) { if (listener === 'all') { for (let i = typeListeners.length - 1; i >= 0; i--) { const entry = typeListeners[i] remove(eventTarget, type, entry.func, entry.options) } return } else { const options = getOptions(optionalArg) for (let i = 0; i < typeListeners.length; i++) { const entry = typeListeners[i] if (entry.func === listener && optionsMatch(entry.options, options)) { eventTarget.removeEventListener( type, listener as any, eventsMethods.supportsOptions ? options : options.capture, ) typeListeners.splice(i, 1) if (typeListeners.length === 0) { delete target.events[type] typeIsEmpty = true } break } } } } if (typeIsEmpty && !Object.keys(target.events).length) { targets.splice(targetIndex, 1) } } function addDelegate( selector: string, context: Node, type: string, listener: ListenerEntry['func'], optionalArg?: any, ) { const options = getOptions(optionalArg) if (!delegatedEvents[type]) { delegatedEvents[type] = [] // add delegate listener functions for (const doc of documents) { add(doc, type, delegateListener) add(doc, type, delegateUseCapture, true) } } const delegates = delegatedEvents[type] let delegate = arr.find(delegates, (d) => d.selector === selector && d.context === context) if (!delegate) { delegate = { selector, context, listeners: [] } delegates.push(delegate) } delegate.listeners.push({ func: listener, options }) } function removeDelegate( selector: string, context: Document | Element, type: string, listener?: ListenerEntry['func'], optionalArg?: any, ) { const options = getOptions(optionalArg) const delegates = delegatedEvents[type] let matchFound = false let index: number if (!delegates) return // count from last index of delegated to 0 for (index = delegates.length - 1; index >= 0; index--) { const cur = delegates[index] // look for matching selector and context Node if (cur.selector === selector && cur.context === context) { const { listeners } = cur // each item of the listeners array is an array: [function, capture, passive] for (let i = listeners.length - 1; i >= 0; i--) { const entry = listeners[i] // check if the listener functions and capture and passive flags match if (entry.func === listener && optionsMatch(entry.options, options)) { // remove the listener from the array of listeners listeners.splice(i, 1) // if all listeners for this target have been removed // remove the target from the delegates array if (!listeners.length) { delegates.splice(index, 1) // remove delegate function from context remove(context, type, delegateListener) remove(context, type, delegateUseCapture, true) } // only remove one listener matchFound = true break } } if (matchFound) { break } } } } // bound to the interactable context when a DOM event // listener is added to a selector interactable function delegateListener(event: Event | FakeEvent, optionalArg?: any) { const options = getOptions(optionalArg) const fakeEvent = new FakeEvent(event as Event) const delegates = delegatedEvents[event.type] const [eventTarget] = pointerUtils.getEventTargets(event as Event) let element: Node = eventTarget // climb up document tree looking for selector matches while (is.element(element)) { for (let i = 0; i < delegates.length; i++) { const cur = delegates[i] const { selector, context } = cur if ( domUtils.matchesSelector(element, selector) && domUtils.nodeContains(context, eventTarget) && domUtils.nodeContains(context, element) ) { const { listeners } = cur fakeEvent.currentTarget = element for (const entry of listeners) { if (optionsMatch(entry.options, options)) { entry.func(fakeEvent) } } } } element = domUtils.parentNode(element) } } function delegateUseCapture(this: Element, event: Event | FakeEvent) { return delegateListener.call(this, event, true) } // for type inferrence return eventsMethods } class FakeEvent implements Partial { currentTarget: Node originalEvent: Event type: string constructor(originalEvent: Event) { this.originalEvent = originalEvent // duplicate the event so that currentTarget can be changed pExtend(this, originalEvent) } preventOriginalDefault() { this.originalEvent.preventDefault() } stopPropagation() { this.originalEvent.stopPropagation() } stopImmediatePropagation() { this.originalEvent.stopImmediatePropagation() } } function getOptions(param: { [index: string]: any } | boolean): { capture: boolean; passive: boolean } { if (!is.object(param)) { return { capture: !!param, passive: false } } return { capture: !!param.capture, passive: !!param.passive, } } function optionsMatch(a: Partial | boolean, b: Partial) { if (a === b) return true if (typeof a === 'boolean') return !!b.capture === a && !!b.passive === false return !!a.capture === !!b.capture && !!a.passive === !!b.passive } export default { id: 'events', install, } ================================================ FILE: packages/@interactjs/core/interactablePreventDefault.spec.ts ================================================ import drag from '@interactjs/actions/drag/plugin' import autoStart from '@interactjs/auto-start/base' import interactablePreventDefault from './interactablePreventDefault' import * as helpers from './tests/_helpers' test('core/interactablePreventDefault', () => { window.PointerEvent = null const { scope, interactable } = helpers.testEnv({ plugins: [interactablePreventDefault, autoStart, drag], }) const { MouseEvent, Event } = scope.window as any interactable.draggable({}) const mouseDown: MouseEvent = new MouseEvent('mousedown', { bubbles: true }) const nativeDragStart: Event = new Event('dragstart', { bubbles: true }) nativeDragStart.preventDefault = jest.fn() scope.document.body.dispatchEvent(mouseDown) scope.document.body.dispatchEvent(nativeDragStart) // native dragstart is prevented on interactable expect(nativeDragStart.preventDefault).toHaveBeenCalledTimes(1) }) ================================================ FILE: packages/@interactjs/core/interactablePreventDefault.ts ================================================ import { matchesSelector, nodeContains } from '@interactjs/utils/domUtils' import is from '@interactjs/utils/is' import { getWindow } from '@interactjs/utils/window' import type { Interactable } from '@interactjs/core/Interactable' import type Interaction from '@interactjs/core/Interaction' import type { Scope } from '@interactjs/core/scope' import type { PointerEventType } from '@interactjs/core/types' type PreventDefaultValue = 'always' | 'never' | 'auto' declare module '@interactjs/core/Interactable' { interface Interactable { preventDefault(newValue: PreventDefaultValue): this preventDefault(): PreventDefaultValue /** * Returns or sets whether to prevent the browser's default behaviour in * response to pointer events. Can be set to: * - `'always'` to always prevent * - `'never'` to never prevent * - `'auto'` to let interact.js try to determine what would be best * * @param newValue - `'always'`, `'never'` or `'auto'` * @returns The current setting or this Interactable */ preventDefault(newValue?: PreventDefaultValue): PreventDefaultValue | this checkAndPreventDefault(event: Event): void } } const preventDefault = function preventDefault(this: Interactable, newValue?: PreventDefaultValue) { if (/^(always|never|auto)$/.test(newValue)) { this.options.preventDefault = newValue return this } if (is.bool(newValue)) { this.options.preventDefault = newValue ? 'always' : 'never' return this } return this.options.preventDefault } as Interactable['preventDefault'] function checkAndPreventDefault(interactable: Interactable, scope: Scope, event: Event) { const setting = interactable.options.preventDefault if (setting === 'never') return if (setting === 'always') { event.preventDefault() return } // setting === 'auto' // if the browser supports passive event listeners and isn't running on iOS, // don't preventDefault of touch{start,move} events. CSS touch-action and // user-select should be used instead of calling event.preventDefault(). if (scope.events.supportsPassive && /^touch(start|move)$/.test(event.type)) { const doc = getWindow(event.target).document const docOptions = scope.getDocOptions(doc) if (!(docOptions && docOptions.events) || docOptions.events.passive !== false) { return } } // don't preventDefault of pointerdown events if (/^(mouse|pointer|touch)*(down|start)/i.test(event.type)) { return } // don't preventDefault on editable elements if ( is.element(event.target) && matchesSelector(event.target, 'input,select,textarea,[contenteditable=true],[contenteditable=true] *') ) { return } event.preventDefault() } function onInteractionEvent({ interaction, event }: { interaction: Interaction; event: PointerEventType }) { if (interaction.interactable) { interaction.interactable.checkAndPreventDefault(event as Event) } } export function install(scope: Scope) { const { Interactable } = scope Interactable.prototype.preventDefault = preventDefault Interactable.prototype.checkAndPreventDefault = function (event) { return checkAndPreventDefault(this, scope, event) } // prevent native HTML5 drag on interact.js target elements scope.interactions.docEvents.push({ type: 'dragstart', listener(event) { for (const interaction of scope.interactions.list) { if ( interaction.element && (interaction.element === event.target || nodeContains(interaction.element, event.target)) ) { interaction.interactable.checkAndPreventDefault(event) return } } }, }) } export default { id: 'core/interactablePreventDefault', install, listeners: ['down', 'move', 'up', 'cancel'].reduce((acc, eventType) => { acc[`interactions:${eventType}`] = onInteractionEvent return acc }, {} as any), } ================================================ FILE: packages/@interactjs/core/interactionFinder.spec.ts ================================================ import finder from './interactionFinder' import * as helpers from './tests/_helpers' test('modifiers/snap', () => { const { interactable, event, coords, scope } = helpers.testEnv() const { body } = scope.document const { list } = scope.interactions const details = { pointer: event, get pointerId(): number { return details.pointer.pointerId }, get pointerType(): string { return details.pointer.pointerType }, eventType: null as string, eventTarget: body, curEventTarget: scope.document, scope, } scope.interactions.new({ pointerType: 'touch' }) scope.interactions.new({ pointerType: 'mouse' }) coords.pointerType = 'mouse' list[0].pointerType = 'mouse' list[2]._interacting = true // [pointerType: mouse] skips inactive mouse and touch interaction expect(list.indexOf(finder.search(details))).toBe(2) list[2]._interacting = false // [pointerType: mouse] returns first idle mouse interaction expect(list.indexOf(finder.search(details))).toBe(0) coords.pointerId = 4 list[1].pointerDown({ ...event } as any, { ...event } as any, body) coords.pointerType = 'touch' // [pointerType: touch] gets interaction with pointerId expect(list.indexOf(finder.search(details))).toBe(1) coords.pointerId = 5 // `[pointerType: touch] returns idle touch interaction without matching pointerId and existing touch interaction has pointer and no target` expect(list.indexOf(finder.search(details))).toBe(1) interactable.options.gesture = { enabled: false } list[1].interactable = interactable // `[pointerType: touch] no result without matching pointerId and existing touch interaction has a pointer and target not gesturable` expect(list.indexOf(finder.search(details))).toBe(-1) interactable.options.gesture = { enabled: true } // `[pointerType: touch] returns idle touch interaction with gesturable target and existing pointer` expect(list.indexOf(finder.search(details))).toBe(1) }) ================================================ FILE: packages/@interactjs/core/interactionFinder.ts ================================================ import * as dom from '@interactjs/utils/domUtils' import type Interaction from '@interactjs/core/Interaction' import type { Scope } from '@interactjs/core/scope' import type { PointerType } from '@interactjs/core/types' export interface SearchDetails { pointer: PointerType pointerId: number pointerType: string eventType: string eventTarget: EventTarget curEventTarget: EventTarget scope: Scope } const finder = { methodOrder: ['simulationResume', 'mouseOrPen', 'hasPointer', 'idle'] as const, search(details: SearchDetails) { for (const method of finder.methodOrder) { const interaction = finder[method](details) if (interaction) { return interaction } } return null }, // try to resume simulation with a new pointer simulationResume({ pointerType, eventType, eventTarget, scope }: SearchDetails) { if (!/down|start/i.test(eventType)) { return null } for (const interaction of scope.interactions.list) { let element = eventTarget as Node if ( interaction.simulation && interaction.simulation.allowResume && interaction.pointerType === pointerType ) { while (element) { // if the element is the interaction element if (element === interaction.element) { return interaction } element = dom.parentNode(element) } } } return null }, // if it's a mouse or pen interaction mouseOrPen({ pointerId, pointerType, eventType, scope }: SearchDetails) { if (pointerType !== 'mouse' && pointerType !== 'pen') { return null } let firstNonActive for (const interaction of scope.interactions.list) { if (interaction.pointerType === pointerType) { // if it's a down event, skip interactions with running simulations if (interaction.simulation && !hasPointerId(interaction, pointerId)) { continue } // if the interaction is active, return it immediately if (interaction.interacting()) { return interaction } // otherwise save it and look for another active interaction else if (!firstNonActive) { firstNonActive = interaction } } } // if no active mouse interaction was found use the first inactive mouse // interaction if (firstNonActive) { return firstNonActive } // find any mouse or pen interaction. // ignore the interaction if the eventType is a *down, and a simulation // is active for (const interaction of scope.interactions.list) { if (interaction.pointerType === pointerType && !(/down/i.test(eventType) && interaction.simulation)) { return interaction } } return null }, // get interaction that has this pointer hasPointer({ pointerId, scope }: SearchDetails) { for (const interaction of scope.interactions.list) { if (hasPointerId(interaction, pointerId)) { return interaction } } return null }, // get first idle interaction with a matching pointerType idle({ pointerType, scope }: SearchDetails) { for (const interaction of scope.interactions.list) { // if there's already a pointer held down if (interaction.pointers.length === 1) { const target = interaction.interactable // don't add this pointer if there is a target interactable and it // isn't gesturable if (target && !(target.options.gesture && target.options.gesture.enabled)) { continue } } // maximum of 2 pointers per interaction else if (interaction.pointers.length >= 2) { continue } if (!interaction.interacting() && pointerType === interaction.pointerType) { return interaction } } return null }, } function hasPointerId(interaction: Interaction, pointerId: number) { return interaction.pointers.some(({ id }) => id === pointerId) } export default finder ================================================ FILE: packages/@interactjs/core/interactions.spec.ts ================================================ import Interaction from './Interaction' import interactions from './interactions' import * as helpers from './tests/_helpers' describe('core/interactions', () => { test('interactions', () => { let scope = helpers.mockScope() const interaction = scope.interactions.new({ pointerType: 'TEST' }) // new Interaction is pushed to scope.interactions expect(scope.interactions.list[0]).toBe(interaction) // interactions object added to scope expect(scope.interactions).toBeInstanceOf(Object) const listeners = scope.interactions.listeners expect(interactions.methodNames.every((m: string) => typeof listeners[m] === 'function')).toBe(true) scope = helpers.mockScope() const newInteraction = scope.interactions.new({}) expect(typeof scope.interactions).toBe('object') expect(typeof scope.interactions.new).toBe('function') expect(newInteraction instanceof Interaction).toBe(true) expect(typeof newInteraction._scopeFire).toBe('function') expect(scope.actions).toBeInstanceOf(Object) expect(scope.actions.map).toEqual({}) expect(scope.actions.methodDict).toEqual({}) }) test('interactions document event options', () => { const { scope } = helpers.testEnv() const doc = scope.document let options = {} scope.browser = { isIOS: false } as any scope.fire('scope:add-document', { doc, scope, options } as any) // no doc options.event.passive is added when not iOS expect(options).toEqual({}) options = {} scope.browser.isIOS = true scope.fire('scope:add-document', { doc, scope, options } as any) // doc options.event.passive is set to false for iOS expect(options).toEqual({ events: { passive: false } }) }) test('interactions removes pointers on targeting removed elements', () => { const { interaction, scope } = helpers.testEnv() const { PointerEvent } = scope.window as any const div1 = scope.document.body.appendChild(scope.document.createElement('div')) const div2 = scope.document.body.appendChild(scope.document.createElement('div')) const touch1Init = { bubbles: true, pointerType: 'touch', pointerId: 1, target: div1 } const touch2Init = { bubbles: true, pointerType: 'touch', pointerId: 2, target: div2 } interaction.pointerType = 'touch' div1.dispatchEvent(new PointerEvent('pointerdown', touch1Init)) div1.dispatchEvent(new PointerEvent('pointermove', touch1Init)) expect(scope.interactions.list).toHaveLength(1) // down pointer added to interaction expect(interaction.pointers).toHaveLength(1) // _latestPointer target is down target expect(interaction._latestPointer.eventTarget).toBe(div1) div1.remove() div2.dispatchEvent(new TouchEvent('touchstart', touch2Init)) // interaction with removed element is reused for new pointer expect(scope.interactions.list).toEqual([interaction]) // pointer on removed element is removed from existing interaction and new pointerdown is added expect(interaction.pointers).toHaveLength(1) }) }) ================================================ FILE: packages/@interactjs/core/interactions.ts ================================================ import browser from '@interactjs/utils/browser' import domObjects from '@interactjs/utils/domObjects' import { nodeContains } from '@interactjs/utils/domUtils' import * as pointerUtils from '@interactjs/utils/pointerUtils' import type { Scope, SignalArgs, Plugin } from '@interactjs/core/scope' import type { ActionName, Listener } from '@interactjs/core/types' /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import './interactablePreventDefault' import interactablePreventDefault from './interactablePreventDefault' import InteractionBase from './Interaction' /* eslint-enable import/no-duplicates */ import type { SearchDetails } from './interactionFinder' import finder from './interactionFinder' declare module '@interactjs/core/scope' { interface Scope { Interaction: typeof InteractionBase interactions: { new: (options: any) => InteractionBase list: Array> listeners: { [type: string]: Listener } docEvents: Array<{ type: string; listener: Listener }> pointerMoveTolerance: number } prevTouchTime: number } interface SignalArgs { 'interactions:find': { interaction: InteractionBase searchDetails: SearchDetails } } } const methodNames = [ 'pointerDown', 'pointerMove', 'pointerUp', 'updatePointer', 'removePointer', 'windowBlur', ] function install(scope: Scope) { const listeners = {} as any for (const method of methodNames) { listeners[method] = doOnInteractions(method, scope) } const pEventTypes = browser.pEventTypes let docEvents: typeof scope.interactions.docEvents if (domObjects.PointerEvent) { docEvents = [ { type: pEventTypes.down, listener: releasePointersOnRemovedEls }, { type: pEventTypes.down, listener: listeners.pointerDown }, { type: pEventTypes.move, listener: listeners.pointerMove }, { type: pEventTypes.up, listener: listeners.pointerUp }, { type: pEventTypes.cancel, listener: listeners.pointerUp }, ] } else { docEvents = [ { type: 'mousedown', listener: listeners.pointerDown }, { type: 'mousemove', listener: listeners.pointerMove }, { type: 'mouseup', listener: listeners.pointerUp }, { type: 'touchstart', listener: releasePointersOnRemovedEls }, { type: 'touchstart', listener: listeners.pointerDown }, { type: 'touchmove', listener: listeners.pointerMove }, { type: 'touchend', listener: listeners.pointerUp }, { type: 'touchcancel', listener: listeners.pointerUp }, ] } docEvents.push({ type: 'blur', listener(event) { for (const interaction of scope.interactions.list) { interaction.documentBlur(event) } }, }) // for ignoring browser's simulated mouse events scope.prevTouchTime = 0 scope.Interaction = class extends InteractionBase { get pointerMoveTolerance() { return scope.interactions.pointerMoveTolerance } set pointerMoveTolerance(value) { scope.interactions.pointerMoveTolerance = value } _now() { return scope.now() } } scope.interactions = { // all active and idle interactions list: [], new(options: { pointerType?: string; scopeFire?: Scope['fire'] }) { options.scopeFire = (name, arg) => scope.fire(name, arg) const interaction = new scope.Interaction(options as Required) scope.interactions.list.push(interaction) return interaction }, listeners, docEvents, pointerMoveTolerance: 1, } function releasePointersOnRemovedEls() { // for all inactive touch interactions with pointers down for (const interaction of scope.interactions.list) { if (!interaction.pointerIsDown || interaction.pointerType !== 'touch' || interaction._interacting) { continue } // if a pointer is down on an element that is no longer in the DOM tree for (const pointer of interaction.pointers) { if (!scope.documents.some(({ doc }) => nodeContains(doc, pointer.downTarget))) { // remove the pointer from the interaction interaction.removePointer(pointer.pointer, pointer.event) } } } } scope.usePlugin(interactablePreventDefault) } function doOnInteractions(method: string, scope: Scope) { return function (event: Event) { const interactions = scope.interactions.list const pointerType = pointerUtils.getPointerType(event) const [eventTarget, curEventTarget] = pointerUtils.getEventTargets(event) const matches: any[] = [] // [ [pointer, interaction], ...] if (/^touch/.test(event.type)) { scope.prevTouchTime = scope.now() // @ts-expect-error for (const changedTouch of event.changedTouches) { const pointer = changedTouch const pointerId = pointerUtils.getPointerId(pointer) const searchDetails: SearchDetails = { pointer, pointerId, pointerType, eventType: event.type, eventTarget, curEventTarget, scope, } const interaction = getInteraction(searchDetails) matches.push([ searchDetails.pointer, searchDetails.eventTarget, searchDetails.curEventTarget, interaction, ]) } } else { let invalidPointer = false if (!browser.supportsPointerEvent && /mouse/.test(event.type)) { // ignore mouse events while touch interactions are active for (let i = 0; i < interactions.length && !invalidPointer; i++) { invalidPointer = interactions[i].pointerType !== 'mouse' && interactions[i].pointerIsDown } // try to ignore mouse events that are simulated by the browser // after a touch event invalidPointer = invalidPointer || scope.now() - scope.prevTouchTime < 500 || // on iOS and Firefox Mobile, MouseEvent.timeStamp is zero if simulated event.timeStamp === 0 } if (!invalidPointer) { const searchDetails = { pointer: event as PointerEvent, pointerId: pointerUtils.getPointerId(event as PointerEvent), pointerType, eventType: event.type, curEventTarget, eventTarget, scope, } const interaction = getInteraction(searchDetails) matches.push([ searchDetails.pointer, searchDetails.eventTarget, searchDetails.curEventTarget, interaction, ]) } } // eslint-disable-next-line no-shadow for (const [pointer, eventTarget, curEventTarget, interaction] of matches) { interaction[method](pointer, event, eventTarget, curEventTarget) } } } function getInteraction(searchDetails: SearchDetails) { const { pointerType, scope } = searchDetails const foundInteraction = finder.search(searchDetails) const signalArg = { interaction: foundInteraction, searchDetails } scope.fire('interactions:find', signalArg) return signalArg.interaction || scope.interactions.new({ pointerType }) } function onDocSignal( { doc, scope, options }: SignalArgs[T], eventMethodName: 'add' | 'remove', ) { const { interactions: { docEvents }, events, } = scope const eventMethod = events[eventMethodName] if (scope.browser.isIOS && !options.events) { options.events = { passive: false } } // delegate event listener for (const eventType in events.delegatedEvents) { eventMethod(doc, eventType, events.delegateListener) eventMethod(doc, eventType, events.delegateUseCapture, true) } const eventOptions = options && options.events for (const { type, listener } of docEvents) { eventMethod(doc, type, listener, eventOptions) } } const interactions: Plugin = { id: 'core/interactions', install, listeners: { 'scope:add-document': (arg) => onDocSignal(arg, 'add'), 'scope:remove-document': (arg) => onDocSignal(arg, 'remove'), 'interactable:unset': ({ interactable }, scope) => { // Stop and destroy related interactions when an Interactable is unset for (let i = scope.interactions.list.length - 1; i >= 0; i--) { const interaction = scope.interactions.list[i] if (interaction.interactable !== interactable) { continue } interaction.stop() scope.fire('interactions:destroy', { interaction }) interaction.destroy() if (scope.interactions.list.length > 2) { scope.interactions.list.splice(i, 1) } } }, }, onDocSignal, doOnInteractions, methodNames, } export default interactions ================================================ FILE: packages/@interactjs/core/options.ts ================================================ import type { Point, Listeners, OrBoolean, Element, Rect } from '@interactjs/core/types' export interface Defaults { base: BaseDefaults perAction: PerActionDefaults actions: ActionDefaults } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ActionDefaults {} export interface BaseDefaults { preventDefault?: 'always' | 'never' | 'auto' deltaSource?: 'page' | 'client' context?: Node getRect?: (element: Element) => Rect } export interface PerActionDefaults { enabled?: boolean origin?: Point | string | Element listeners?: Listeners allowFrom?: string | Element ignoreFrom?: string | Element } export type Options = Partial & Partial & { [P in keyof ActionDefaults]?: Partial } export interface OptionsArg extends BaseDefaults, OrBoolean> {} export const defaults: Defaults = { base: { preventDefault: 'auto', deltaSource: 'page', }, perAction: { enabled: false, origin: { x: 0, y: 0 }, }, actions: {} as ActionDefaults, } ================================================ FILE: packages/@interactjs/core/package.json ================================================ { "name": "@interactjs/core", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/core" }, "peerDependencies": { "@interactjs/utils": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/core/scope.spec.ts ================================================ import type { ActionName } from '@interactjs/core/types' import * as helpers from './tests/_helpers' describe('core/scope', () => { test('usePlugin', () => { const { scope } = helpers.testEnv() const plugin1 = { id: '1', listeners: {} } const plugin2 = { id: '2', listeners: {} } const plugin3 = { id: '3', listeners: {}, before: ['2'] } const plugin4 = { id: '4', listeners: {}, before: ['2', '3'] } const initialListeners = scope.listenerMaps.map((l) => l.id) scope.usePlugin(plugin1) scope.usePlugin(plugin2) scope.usePlugin(plugin3) scope.usePlugin(plugin4) expect(scope.listenerMaps.map((l) => l.id)).toEqual([...initialListeners, '1', '4', '3', '2']) }) test('interactable unset', () => { const { scope, interactable, interaction, event } = helpers.testEnv() const interactable2 = scope.interactables.new('x') expect(scope.interactables.list).toContain(interactable) expect(scope.interactables.list).toContain(interactable2) ;(interactable.options as any).test = { enabled: true } interaction.pointerDown(event, event, scope.document.body) interaction.start({ name: 'test' as ActionName }, interactable, scope.document.body) const started = interaction._interacting interactable.unset() const stopped = !interaction._interacting expect(scope.interactables.list).not.toContain(interactable) expect(scope.interactables.list).toContain(interactable2) // interaction is stopped on interactable.unset() expect(started && stopped).toBe(true) // repeated call to unset interactable.unset() expect(scope.interactables.list).not.toContain(interactable) expect(scope.interactables.list).toContain(interactable2) }) }) ================================================ FILE: packages/@interactjs/core/scope.ts ================================================ import browser from '@interactjs/utils/browser' import clone from '@interactjs/utils/clone' import domObjects from '@interactjs/utils/domObjects' import extend from '@interactjs/utils/extend' import is from '@interactjs/utils/is' import raf from '@interactjs/utils/raf' import * as win from '@interactjs/utils/window' import type Interaction from '@interactjs/core/Interaction' import { Eventable } from './Eventable' /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import './events' import './interactions' import events from './events' import { Interactable as InteractableBase } from './Interactable' import { InteractableSet } from './InteractableSet' import { InteractEvent } from './InteractEvent' import interactions from './interactions' /* eslint-enable import/no-duplicates */ import { createInteractStatic } from './InteractStatic' import type { OptionsArg } from './options' import { defaults } from './options' import type { Actions } from './types' export interface SignalArgs { 'scope:add-document': DocSignalArg 'scope:remove-document': DocSignalArg 'interactable:unset': { interactable: InteractableBase } 'interactable:set': { interactable: InteractableBase; options: OptionsArg } 'interactions:destroy': { interaction: Interaction } } export type ListenerName = keyof SignalArgs export type ListenerMap = { [P in ListenerName]?: (arg: SignalArgs[P], scope: Scope, signalName: P) => void | boolean } interface DocSignalArg { doc: Document window: Window scope: Scope options: Record } export interface Plugin { [key: string]: any id?: string listeners?: ListenerMap before?: string[] install?(scope: Scope, options?: any): void } /** @internal */ export class Scope { id = `__interact_scope_${Math.floor(Math.random() * 100)}` isInitialized = false listenerMaps: Array<{ map: ListenerMap id?: string }> = [] browser = browser defaults = clone(defaults) as typeof defaults Eventable = Eventable actions: Actions = { map: {}, phases: { start: true, move: true, end: true, }, methodDict: {} as any, phaselessTypes: {}, } interactStatic = createInteractStatic(this) InteractEvent = InteractEvent Interactable: typeof InteractableBase interactables = new InteractableSet(this) // main window _win!: Window // main document document!: Document // main window window!: Window // all documents being listened to documents: Array<{ doc: Document; options: any }> = [] _plugins: { list: Plugin[] map: { [id: string]: Plugin } } = { list: [], map: {}, } constructor() { const scope = this this.Interactable = class extends InteractableBase { get _defaults() { return scope.defaults } set(this: T, options: OptionsArg) { super.set(options) scope.fire('interactable:set', { options, interactable: this, }) return this } unset(this: InteractableBase) { super.unset() const index = scope.interactables.list.indexOf(this) if (index < 0) return scope.interactables.list.splice(index, 1) scope.fire('interactable:unset', { interactable: this }) } } } addListeners(map: ListenerMap, id?: string) { this.listenerMaps.push({ id, map }) } fire(name: T, arg: SignalArgs[T]): void | false { for (const { map: { [name]: listener }, } of this.listenerMaps) { if (!!listener && listener(arg as any, this, name as never) === false) { return false } } } onWindowUnload = (event: BeforeUnloadEvent) => this.removeDocument(event.target as Document) init(window: Window | typeof globalThis) { return this.isInitialized ? this : initScope(this, window) } pluginIsInstalled(plugin: Plugin) { const { id } = plugin return id ? !!this._plugins.map[id] : this._plugins.list.indexOf(plugin) !== -1 } usePlugin(plugin: Plugin, options?: { [key: string]: any }) { if (!this.isInitialized) { return this } if (this.pluginIsInstalled(plugin)) { return this } if (plugin.id) { this._plugins.map[plugin.id] = plugin } this._plugins.list.push(plugin) if (plugin.install) { plugin.install(this, options) } if (plugin.listeners && plugin.before) { let index = 0 const len = this.listenerMaps.length const before = plugin.before.reduce((acc, id) => { acc[id] = true acc[pluginIdRoot(id)] = true return acc }, {}) for (; index < len; index++) { const otherId = this.listenerMaps[index].id if (otherId && (before[otherId] || before[pluginIdRoot(otherId)])) { break } } this.listenerMaps.splice(index, 0, { id: plugin.id, map: plugin.listeners }) } else if (plugin.listeners) { this.listenerMaps.push({ id: plugin.id, map: plugin.listeners }) } return this } addDocument(doc: Document, options?: any): void | false { // do nothing if document is already known if (this.getDocIndex(doc) !== -1) { return false } const window = win.getWindow(doc) options = options ? extend({}, options) : {} this.documents.push({ doc, options }) this.events.documents.push(doc) // don't add an unload event for the main document // so that the page may be cached in browser history if (doc !== this.document) { this.events.add(window, 'unload', this.onWindowUnload) } this.fire('scope:add-document', { doc, window, scope: this, options }) } removeDocument(doc: Document) { const index = this.getDocIndex(doc) const window = win.getWindow(doc) const options = this.documents[index].options this.events.remove(window, 'unload', this.onWindowUnload) this.documents.splice(index, 1) this.events.documents.splice(index, 1) this.fire('scope:remove-document', { doc, window, scope: this, options }) } getDocIndex(doc: Document) { for (let i = 0; i < this.documents.length; i++) { if (this.documents[i].doc === doc) { return i } } return -1 } getDocOptions(doc: Document) { const docIndex = this.getDocIndex(doc) return docIndex === -1 ? null : this.documents[docIndex].options } now() { return (((this.window as any).Date as typeof Date) || Date).now() } } // Keep Scope class internal, but expose minimal interface to avoid broken types when Scope is stripped out export interface Scope { fire(name: T, arg: SignalArgs[T]): void | false } /** @internal */ export function initScope(scope: Scope, window: Window | typeof globalThis) { scope.isInitialized = true if (is.window(window)) { win.init(window) } domObjects.init(window) browser.init(window) raf.init(window) // @ts-expect-error scope.window = window scope.document = window.document scope.usePlugin(interactions) scope.usePlugin(events) return scope } function pluginIdRoot(id: string) { return id && id.replace(/\/.*$/, '') } ================================================ FILE: packages/@interactjs/core/tests/_helpers.ts ================================================ import extend from '@interactjs/utils/extend' import is from '@interactjs/utils/is' import * as pointerUtils from '@interactjs/utils/pointerUtils' import type { PointerType, Rect, Target, ActionName, ActionProps } from '@interactjs/core/types' import type { Plugin } from '../scope' import { Scope } from '../scope' let counter = 0 export function unique() { return counter++ } export function uniqueProps(obj: any) { for (const prop in obj) { if (!obj.hasOwnProperty(prop)) { continue } if (is.object(obj)) { uniqueProps(obj[prop]) } else { obj[prop] = counter++ } } } export function newCoordsSet(n = 0) { return { start: { page: { x: n++, y: n++ }, client: { x: n++, y: n++ }, timeStamp: n++, }, cur: { page: { x: n++, y: n++ }, client: { x: n++, y: n++ }, timeStamp: n++, }, prev: { page: { x: n++, y: n++ }, client: { x: n++, y: n++ }, timeStamp: n++, }, delta: { page: { x: n++, y: n++ }, client: { x: n++, y: n++ }, timeStamp: n++, }, velocity: { page: { x: n++, y: n++ }, client: { x: n++, y: n++ }, timeStamp: n++, }, } } export function newPointer(n = 50) { return { pointerId: n++, pageX: n++, pageY: n++, clientX: n++, clientY: n++, } as PointerType } export function mockScope({ document = window.document } = {} as any) { const window = document.defaultView const scope = new Scope().init(window) extend(scope.actions.phaselessTypes, { teststart: true, testmove: true, testend: true }) return scope } export function getProps(src: T, props: readonly K[]) { return props.reduce( (acc, prop) => { if (prop in src) { acc[prop] = src[prop] } return acc }, {} as Pick, ) } export function testEnv({ plugins = [], target, rect, document = window.document, }: { plugins?: Plugin[] target?: T rect?: Rect document?: Document } = {}) { const scope = mockScope({ document }) for (const plugin of plugins) { scope.usePlugin(plugin) } if (!target) { ;(target as unknown as HTMLElement) = scope.document.body } const interaction = scope.interactions.new({}) const interactable = scope.interactables.new(target) const coords: pointerUtils.MockCoords = pointerUtils.newCoords() coords.target = target const event = pointerUtils.coordsToEvent(coords) if (rect) { interactable.rectChecker(() => ({ ...rect })) } return { scope, interaction, target: target as T extends undefined ? HTMLElement : T, interactable, coords, event, interact: scope.interactStatic, start: (action: ActionProps) => interaction.start(action, interactable, target as HTMLElement), stop: () => interaction.stop(), down: () => interaction.pointerDown(event, event, target as HTMLElement), move: (force?: boolean) => force ? interaction.move() : interaction.pointerMove(event, event, target as HTMLElement), up: () => interaction.pointerUp(event, event, target as HTMLElement, target as HTMLElement), } } export function timeout(n: number) { return new Promise((resolve) => setTimeout(resolve, n)) } export function ltrbwh( left: number, top: number, right: number, bottom: number, width: number, height: number, ) { return { left, top, right, bottom, width, height } } ================================================ FILE: packages/@interactjs/core/types.ts ================================================ import type Interaction from '@interactjs/core/Interaction' import type { Interactable } from './Interactable' import type { PhaseMap, InteractEvent } from './InteractEvent' import type { NativePointerEvent as NativePointerEvent_ } from './NativeTypes' export type OrBoolean = { [P in keyof T]: T[P] | boolean } export type Element = HTMLElement | SVGElement export type Context = Document | Element export type EventTarget = Window | Document | Element export type Target = EventTarget | string export interface Point { x: number y: number } export interface Size { width: number height: number } export interface Rect { top: number left: number bottom: number right: number width?: number height?: number } export type FullRect = Required export type RectFunction = (...args: T) => Rect | Element export type RectResolvable = Rect | string | Element | RectFunction export type Dimensions = Point & Size export interface CoordsSetMember { page: Point client: Point timeStamp: number } export interface CoordsSet { cur: CoordsSetMember prev: CoordsSetMember start: CoordsSetMember delta: CoordsSetMember velocity: CoordsSetMember } export interface HasGetRect { getRect(element: Element): Rect } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ActionMap {} export type ActionName = keyof ActionMap export interface Actions { map: ActionMap phases: PhaseMap methodDict: Record phaselessTypes: { [type: string]: true } } export interface ActionProps { name: T axis?: 'x' | 'y' | 'xy' | null edges?: EdgeOptions | null } export interface InertiaOption { resistance?: number minSpeed?: number endSpeed?: number allowResume?: boolean smoothEndDuration?: number } export type InertiaOptions = InertiaOption | boolean export interface EdgeOptions { top?: boolean | string | Element left?: boolean | string | Element bottom?: boolean | string | Element right?: boolean | string | Element } export type CursorChecker = ( action: ActionProps, interactable: Interactable, element: Element, interacting: boolean, ) => string export interface ActionMethod { (this: Interactable): T (this: Interactable, options?: Partial> | boolean): typeof this } export interface OptionMethod { (this: Interactable): T // eslint-disable-next-line no-undef (this: Interactable, options: T): typeof this } export type ActionChecker = ( pointerEvent: any, defaultAction: string, interactable: Interactable, element: Element, interaction: Interaction, ) => ActionProps export type OriginFunction = (target: Element) => Rect export interface PointerEventsOptions { holdDuration?: number allowFrom?: string ignoreFrom?: string origin?: Rect | Point | string | Element | OriginFunction } export type RectChecker = (element: Element) => Rect export type NativePointerEventType = typeof NativePointerEvent_ export type PointerEventType = MouseEvent | TouchEvent | Partial | InteractEvent export type PointerType = MouseEvent | Touch | Partial | InteractEvent export type EventTypes = string | ListenerMap | Array export type Listener = (...args: any[]) => any export type Listeners = ListenerMap | ListenerMap[] export type ListenersArg = Listener | ListenerMap | Array export interface ListenerMap { [index: string]: ListenersArg | ListenersArg[] } export type ArrayElementType = T extends Array ? P : never ================================================ FILE: packages/@interactjs/dev-tools/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/dev-tools/babel-plugin-prod.js ================================================ /* global process, __dirname */ const path = require('path') const PROD_EXT = '.prod' function fixImportSource ({ node: { source } }, { filename }) { if (shouldIgnoreImport(source)) return let resolvedShort = '' try { const paths = [filename && path.dirname(filename), __dirname, process.cwd()].filter((p) => !!p) const resolved = require.resolve(source.value, { paths }) const resolvedWithoutScopePath = resolved.replace(/.*[\\/]@interactjs[\\/]/, '') resolvedShort = path .join('@interactjs', resolvedWithoutScopePath) // windows path to posix .replace(/\\/g, '/') source.value = resolvedShort.replace(/(\.js)?$/, PROD_EXT) } catch (e) {} } function babelPluginInteractjsProd () { if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console console.warn( "[@interactjs/dev-tools] You're using the production plugin in the development environment. You might lose out on some helpful hints!", ) } return { visitor: { ImportDeclaration: fixImportSource, ExportNamedDeclaration: fixImportSource, ExportAllDeclaration: fixImportSource, ExportDefaultSpecifier: fixImportSource, }, } } function shouldIgnoreImport (source) { return ( !source || // only change @interactjs scoped imports !source.value.startsWith('@interactjs/') || // ignore imports of prod files source.value.endsWith(PROD_EXT) || source.value.endsWith(PROD_EXT + '.js') ) } module.exports = babelPluginInteractjsProd Object.assign(module.exports, { default: babelPluginInteractjsProd, fixImportSource, }) ================================================ FILE: packages/@interactjs/dev-tools/babel-plugin-prod.spec.ts ================================================ /** @jest-environment node */ import * as babel from '@babel/core' import proposalExportDefaultFrom from '@babel/plugin-proposal-export-default-from' import babelPluginProd, { fixImportSource } from './babel-plugin-prod' describe('@dev-tools/prod/babel-plugin-prod', () => { const filename = require.resolve('@interactjs/_dev/test/fixtures/babelPluginProject/index.js') const cases = [ { module: 'x', expected: 'x', message: 'non @interact/* package unchanged', }, { module: 'interact', expected: 'interact', message: 'unscoped interact import unchanged', }, { module: '@interactjs/NONEXISTENT_PACKAGE', expected: '@interactjs/NONEXISTENT_PACKAGE', message: 'missing package unchanged', }, { module: '@interactjs/a/NONEXISTENT_MODULE', expected: '@interactjs/a/NONEXISTENT_MODULE', message: 'import of missing module unchanged', }, { module: '@interactjs/a', expected: '@interactjs/a/package-main-file.prod', message: 'package main module', }, { module: '@interactjs/a/a', expected: '@interactjs/a/a.prod', message: 'package root-level non index module', }, { module: '@interactjs/a/b', expected: '@interactjs/a/b/index.prod', message: 'nested index module', }, { module: '@interactjs/a/b/b', expected: '@interactjs/a/b/b.prod', message: 'package nested non index module', }, ] for (const { module, expected, message } of cases) { // eslint-disable-next-line jest/valid-title test(message, () => { const source = { value: module } fixImportSource({ node: { source } }, { filename }) expect(source.value).toBe(expected) }) } test('transforms code when used in babel config', () => { expect( babel.transform( [ 'import "@interactjs/a/a";', 'import a, { b } from "@interactjs/a/a";', 'export b from "@interactjs/a/a";', 'export * from "@interactjs/a/a";', ].join('\n'), { babelrc: false, configFile: false, plugins: [babelPluginProd, [proposalExportDefaultFrom, { loose: true }]], filename, sourceType: 'module', }, ).code, ).toEqual( [ 'import "@interactjs/a/a.prod";', 'import a, { b } from "@interactjs/a/a.prod";', 'export { default as b } from "@interactjs/a/a.prod";', 'export * from "@interactjs/a/a.prod";', ].join('\n'), ) }) }) ================================================ FILE: packages/@interactjs/dev-tools/devTools.spec.ts ================================================ import drag from '@interactjs/actions/drag/plugin' import resize from '@interactjs/actions/resize/plugin' import * as helpers from '@interactjs/core/tests/_helpers' import type { Check, Logger } from './plugin' import devTools from './plugin' const { checks, links, prefix } = devTools const checkMap = checks.reduce( (acc, check) => { acc[check.name] = check return acc }, {} as { [name: string]: Check }, ) test('devTools', () => { const devToolsWithLogger = { install: (s) => s.usePlugin(devTools, { logger: { warn(...args: any[]) { log(args, 'warn') }, log(...args: any[]) { log(args, 'log') }, error(...args: any[]) { log(args, 'error') }, }, }), } const { scope, interaction, interactable, target: element, down, start, move, stop, } = helpers.testEnv({ plugins: [devToolsWithLogger, drag, resize], }) const logs: Array<{ args: any[]; type: keyof Logger }> = [] function log(args: any, type: any) { logs.push({ args, type }) } scope.usePlugin(drag) scope.usePlugin(resize) interactable.draggable(true).resizable({ onmove: () => {} }) down() start({ name: 'drag' }) // warning about missing touchAction expect(logs[0]).toEqual({ args: [prefix + checkMap.touchAction.text, element, links.touchAction], type: 'warn', }) // warning about missing move listeners expect(logs[1]).toEqual({ args: [prefix + checkMap.noListeners.text, 'drag', interactable], type: 'warn' }) stop() // resolve touchAction element.style.touchAction = 'none' // resolve missing listeners interactable.on('dragmove', () => {}) interaction.start({ name: 'resize' }, interactable, element) move() stop() // warning about resizing without "box-sizing: none" expect(logs[2]).toEqual({ args: [prefix + checkMap.boxSizing.text, element, links.boxSizing], type: 'warn', }) logs.splice(0) interaction.start({ name: 'resize' }, interactable, element) move() stop() interactable.options.devTools.ignore = { boxSizing: true } interaction.start({ name: 'resize' }, interactable, element) move() stop() // warning removed with options.devTools.ignore expect(logs).toHaveLength(1) logs.splice(0) // resolve boxSizing interactable.options.devTools.ignore = {} element.style.boxSizing = 'border-box' interaction.start({ name: 'resize' }, interactable, element) move(true) stop() interaction.start({ name: 'drag' }, interactable, element) move() stop() // no warnings when issues are resolved expect(logs).toHaveLength(0) }) ================================================ FILE: packages/@interactjs/dev-tools/package.json ================================================ { "name": "@interactjs/dev-tools", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/dev-tools" }, "peerDependencies": { "@interactjs/modifiers": "1.10.27", "@interactjs/utils": "1.10.27" }, "optionalDependencies": { "@interactjs/interact": "1.10.27", "vue": "3" }, "devDependencies": { "vue": "next" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/dev-tools/plugin.ts ================================================ import type Interaction from '@interactjs/core/Interaction' import type { Scope, Plugin } from '@interactjs/core/scope' import type { Element, OptionMethod } from '@interactjs/core/types' import domObjects from '@interactjs/utils/domObjects' import { parentNode } from '@interactjs/utils/domUtils' import extend from '@interactjs/utils/extend' import is from '@interactjs/utils/is' import isNonNativeEvent from '@interactjs/utils/isNonNativeEvent' import normalizeListeners from '@interactjs/utils/normalizeListeners' import * as win from '@interactjs/utils/window' declare module '@interactjs/core/scope' { interface Scope { logger: Logger } } declare module '@interactjs/core/options' { interface BaseDefaults { devTools?: DevToolsOptions } } declare module '@interactjs/core/Interactable' { interface Interactable { devTools: OptionMethod } } export interface DevToolsOptions { ignore: { [P in keyof typeof CheckName]?: boolean } } export interface Logger { warn: (...args: any[]) => void error: (...args: any[]) => void log: (...args: any[]) => void } export interface Check { name: CheckName text: string perform: (interaction: Interaction) => boolean getInfo: (interaction: Interaction) => any[] } enum CheckName { touchAction = 'touchAction', boxSizing = 'boxSizing', noListeners = 'noListeners', } const prefix = '[interact.js] ' const links = { touchAction: 'https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action', boxSizing: 'https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing', } // eslint-disable-next-line no-undef const isProduction = process.env.NODE_ENV === 'production' function install(scope: Scope, { logger }: { logger?: Logger } = {}) { const { Interactable, defaults } = scope scope.logger = logger || console defaults.base.devTools = { ignore: {}, } Interactable.prototype.devTools = function (options?: object) { if (options) { extend(this.options.devTools, options) return this } return this.options.devTools } // can't set native events on non string targets without `addEventListener` prop const { _onOff } = Interactable.prototype Interactable.prototype._onOff = function (method, typeArg, listenerArg, options, filter) { if (is.string(this.target) || this.target.addEventListener) { return _onOff.call(this, method, typeArg, listenerArg, options, filter) } if (is.object(typeArg) && !is.array(typeArg)) { options = listenerArg listenerArg = null } const normalizedListeners = normalizeListeners(typeArg, listenerArg, filter) for (const type in normalizedListeners) { if (isNonNativeEvent(type, scope.actions)) continue scope.logger.warn( prefix + `Can't add native "${type}" event listener to target without \`addEventListener(type, listener, options)\` prop.`, ) } return _onOff.call(this, method, normalizedListeners, options) } } const checks: Check[] = [ { name: CheckName.touchAction, perform({ element }) { return !!element && !parentHasStyle(element, 'touchAction', /pan-|pinch|none/) }, getInfo({ element }) { return [element, links.touchAction] }, text: 'Consider adding CSS "touch-action: none" to this element\n', }, { name: CheckName.boxSizing, perform(interaction) { const { element } = interaction return ( interaction.prepared.name === 'resize' && element instanceof domObjects.HTMLElement && !hasStyle(element, 'boxSizing', /border-box/) ) }, text: 'Consider adding CSS "box-sizing: border-box" to this resizable element', getInfo({ element }) { return [element, links.boxSizing] }, }, { name: CheckName.noListeners, perform(interaction) { const actionName = interaction.prepared.name const moveListeners = interaction.interactable?.events.types[`${actionName}move`] || [] return !moveListeners.length }, getInfo(interaction) { return [interaction.prepared.name, interaction.interactable] }, text: 'There are no listeners set for this action', }, ] function hasStyle(element: HTMLElement, prop: keyof CSSStyleDeclaration, styleRe: RegExp) { const value = element.style[prop] || win.window.getComputedStyle(element)[prop] return styleRe.test((value || '').toString()) } function parentHasStyle(element: Element, prop: keyof CSSStyleDeclaration, styleRe: RegExp) { let parent = element as HTMLElement while (is.element(parent)) { if (hasStyle(parent, prop, styleRe)) { return true } parent = parentNode(parent) as HTMLElement } return false } const id = 'dev-tools' const defaultExport: Plugin = isProduction ? { id, install: () => {} } : { id, install, listeners: { 'interactions:action-start': ({ interaction }, scope) => { for (const check of checks) { const options = interaction.interactable && interaction.interactable.options if ( !(options && options.devTools && options.devTools.ignore[check.name]) && check.perform(interaction) ) { scope.logger.warn(prefix + check.text, ...check.getInfo(interaction)) } } }, }, checks, CheckName, links, prefix, } export default defaultExport ================================================ FILE: packages/@interactjs/dev-tools/visualizer/plugin.stub.ts ================================================ export default {} ================================================ FILE: packages/@interactjs/dev-tools/visualizer/plugin.ts ================================================ export default {} ================================================ FILE: packages/@interactjs/dev-tools/visualizer/visualizer.spec.stub.ts ================================================ test.skip('visualizer', () => {}) ================================================ FILE: packages/@interactjs/dev-tools/visualizer/visualizer.spec.ts ================================================ test.skip('visualizer', () => {}) ================================================ FILE: packages/@interactjs/dev-tools/visualizer/vueModules.stub.ts ================================================ export {} ================================================ FILE: packages/@interactjs/dev-tools/visualizer/vueModules.ts ================================================ export {} ================================================ FILE: packages/@interactjs/inertia/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/inertia/inertia.spec.ts ================================================ import drag from '@interactjs/actions/drag/plugin' import type { EventPhase, InteractEvent } from '@interactjs/core/InteractEvent' import * as helpers from '@interactjs/core/tests/_helpers' import extend from '@interactjs/utils/extend' import inertia from './plugin' test('inertia', () => { const { scope, interaction, down, start, move, up, interactable, coords } = helpers.testEnv({ plugins: [inertia, drag], rect: { left: 0, top: 0, bottom: 100, right: 100 }, }) const state = interaction.inertia const modifierChange = 5 const changeModifier = { options: { endOnly: false }, methods: { set({ coords: modifierCoords, phase }: any) { modifierCoords.x = modifierChange modifierCoords.y = modifierChange modifierCallPhases.push(phase) }, }, } let fired: Array> = [] let modifierCallPhases: EventPhase[] = [] coords.client = coords.page scope.now = () => coords.timeStamp interactable.draggable({ inertia: true }).on( Object.keys(scope.actions.phases).map((p) => `drag${p}`), (e) => fired.push(e), ) // test inertia without modifiers or throw downStartMoveUp({ x: 100, y: 0, dt: 1000 }) // { modifiers: [] } && !thrown: inertia is not activated expect(state.active).toBe(false) // test inertia without modifiers and with throw downStartMoveUp({ x: 100, y: 0, dt: 10 }) // thrown: inertia is activated expect(state.active && state.timeout).toBeTruthy() interactable.draggable({ modifiers: [changeModifier as any] }) // test inertia with { endOnly: false } modifier and with throw downStartMoveUp({ x: 100, y: 0, dt: 10 }) // { endOnly: false } && thrown: modifier is called from pointerUp inertia calc and phase expect(modifierCallPhases).toEqual(['move', 'inertiastart', 'inertiastart']) // { endOnly: false } && thrown: move, inertiastart, and end InteractEvents are modified expect(fired.map(({ page, type }) => ({ ...page, type }))).toEqual([ { x: 0, y: 0, type: 'dragstart' }, { x: modifierChange, y: modifierChange, type: 'dragmove' }, { x: modifierChange, y: modifierChange, type: 'draginertiastart' }, ]) // test inertia with { endOnly: true } modifier and with throw changeModifier.options.endOnly = true downStartMoveUp({ x: 100, y: 0, dt: 10 }) // { endOnly: true } && thrown: modifier is called from pointerUp inertia calc expect(modifierCallPhases).toEqual(['inertiastart']) // { endOnly: true } && thrown: inertia target coords are correct expect(state.modifiedOffset).toEqual({ // modified target minus move coords x: modifierChange - 100, y: modifierChange - 0, }) // test smoothEnd with { endOnly: false } modifier changeModifier.options.endOnly = false downStartMoveUp({ x: 1, y: 0, dt: 1000 }) // { endOnly: false } && !thrown: inertia smoothEnd is not activated expect(state.active).toBe(false) // { endOnly: false } && !thrown: modifier is called from pointerUp expect(modifierCallPhases).toEqual(['move', 'inertiastart']) // test smoothEnd with { endOnly: true } modifier changeModifier.options.endOnly = true downStartMoveUp({ x: 1, y: 0, dt: 1000 }) // { endOnly: true } && !thrown: inertia smoothEnd is activated expect(state.active).toBe(true) // { endOnly: true } && !thrown: modifier is called from pointerUp smooth end check expect(modifierCallPhases).toEqual(['inertiastart']) interactable.draggable({ modifiers: [ { options: { endOnly: true }, methods: { set({ coords: modifiedCoords, phase }) { extend(modifiedCoords, { x: 300, y: 400 }) modifierCallPhases.push(phase) }, }, enable: null, disable: null, }, ], }) downStartMoveUp({ x: 50, y: 70, dt: 1000 }) coords.timeStamp = 100 expect(state.targetOffset).toEqual({ x: 250, y: 330 }) extend(coords.page, { x: 50, y: 100 }) down() // inertia is stopped on resume expect(interaction._interacting && !state.active).toBe(true) // interaction coords are updated to down coords on resume expect({ coords: interaction.coords.cur.page, rect: interaction.rect }).toEqual({ coords: coords.page, rect: { left: 50, top: 70, right: 150, bottom: 170, width: 100, height: 100 }, }) // action resume event coords are set correctly expect(lastEvent().page).toEqual(coords.page) move() // interaction coords are correct on duplicate move after resume expect({ coords: interaction.coords.cur.page, rect: interaction.rect }).toEqual({ coords: coords.page, rect: { left: 50, top: 70, right: 150, bottom: 170, width: 100, height: 100 }, }) // second release inertia target is not the modified target // second release inertia target is not the pointer event coords // action move event coords on duplicate move after resume is correct expect(lastEvent().page).toEqual(coords.page) extend(coords.page, { x: 200, y: 250 }) move() up() expect(state.targetOffset).not.toEqual(coords.page) expect(state.targetOffset).not.toEqual({ x: 300, y: 400 }) // inertiastart is fired at non preEnd modified coords expect(helpers.getProps(lastEvent(), ['type', 'page', 'rect'] as const)).toEqual({ type: 'draginertiastart', page: coords.page, rect: { left: 200, top: 220, right: 300, bottom: 320, width: 100, height: 100 }, }) down() extend(coords.page, { x: 150, y: 400 }) move() // interaction coords after second resume are correct expect({ coords: interaction.coords.cur.page, rect: interaction.rect }).toEqual({ coords: coords.page, rect: { left: 150, top: 370, right: 250, bottom: 470, width: 100, height: 100 }, }) // action move event after second resume is fired at non preEnd modified coords expect(helpers.getProps(lastEvent(), ['type', 'page', 'rect'] as const)).toEqual({ type: 'dragmove', page: coords.page, rect: { left: 150, top: 370, right: 250, bottom: 470, width: 100, height: 100 }, }) interaction.stop() function downStartMoveUp({ x, y, dt }: any) { fired = [] modifierCallPhases = [] coords.timeStamp = 0 interaction.stop() Object.assign(coords.page, { x: 0, y: 0 }) down() start({ name: 'drag' }) Object.assign(coords.page, { x, y }) coords.timeStamp = dt move() up() } function lastEvent() { return fired[fired.length - 1] } }) ================================================ FILE: packages/@interactjs/inertia/package.json ================================================ { "name": "@interactjs/inertia", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/inertia" }, "dependencies": { "@interactjs/offset": "1.10.27" }, "peerDependencies": { "@interactjs/core": "1.10.27", "@interactjs/modifiers": "1.10.27", "@interactjs/utils": "1.10.27" }, "optionalDependencies": { "@interactjs/interact": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/inertia/plugin.ts ================================================ import type { Interaction, DoPhaseArg } from '@interactjs/core/Interaction' import type { Scope, SignalArgs, Plugin } from '@interactjs/core/scope' import type { ActionName, Point, PointerEventType } from '@interactjs/core/types' /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import '@interactjs/modifiers/base' import '@interactjs/offset/plugin' import * as modifiers from '@interactjs/modifiers/base' import { Modification } from '@interactjs/modifiers/Modification' import type { ModifierArg } from '@interactjs/modifiers/types' import offset from '@interactjs/offset/plugin' /* eslint-enable import/no-duplicates */ import * as dom from '@interactjs/utils/domUtils' import hypot from '@interactjs/utils/hypot' import is from '@interactjs/utils/is' import { copyCoords } from '@interactjs/utils/pointerUtils' import raf from '@interactjs/utils/raf' declare module '@interactjs/core/InteractEvent' { interface PhaseMap { resume?: true inertiastart?: true } } declare module '@interactjs/core/Interaction' { interface Interaction { inertia?: InertiaState } } declare module '@interactjs/core/options' { interface PerActionDefaults { inertia?: { enabled?: boolean resistance?: number // the lambda in exponential decay minSpeed?: number // target speed must be above this for inertia to start endSpeed?: number // the speed at which inertia is slow enough to stop allowResume?: true // allow resuming an action in inertia phase smoothEndDuration?: number // animate to snap/restrict endOnly if there's no inertia } } } declare module '@interactjs/core/scope' { interface SignalArgs { 'interactions:before-action-inertiastart': Omit, 'iEvent'> 'interactions:action-inertiastart': DoPhaseArg 'interactions:after-action-inertiastart': DoPhaseArg 'interactions:before-action-resume': Omit, 'iEvent'> 'interactions:action-resume': DoPhaseArg 'interactions:after-action-resume': DoPhaseArg } } function install(scope: Scope) { const { defaults } = scope scope.usePlugin(offset) scope.usePlugin(modifiers.default) scope.actions.phases.inertiastart = true scope.actions.phases.resume = true defaults.perAction.inertia = { enabled: false, resistance: 10, // the lambda in exponential decay minSpeed: 100, // target speed must be above this for inertia to start endSpeed: 10, // the speed at which inertia is slow enough to stop allowResume: true, // allow resuming an action in inertia phase smoothEndDuration: 300, // animate to snap/restrict endOnly if there's no inertia } } export class InertiaState { active = false isModified = false smoothEnd = false allowResume = false modification!: Modification modifierCount = 0 modifierArg!: ModifierArg startCoords!: Point t0 = 0 v0 = 0 te = 0 targetOffset!: Point modifiedOffset!: Point currentOffset!: Point lambda_v0? = 0 // eslint-disable-line camelcase one_ve_v0? = 0 // eslint-disable-line camelcase timeout!: number readonly interaction: Interaction constructor(interaction: Interaction) { this.interaction = interaction } start(event: PointerEventType) { const { interaction } = this const options = getOptions(interaction) if (!options || !options.enabled) { return false } const { client: velocityClient } = interaction.coords.velocity const pointerSpeed = hypot(velocityClient.x, velocityClient.y) const modification = this.modification || (this.modification = new Modification(interaction)) modification.copyFrom(interaction.modification) this.t0 = interaction._now() this.allowResume = options.allowResume this.v0 = pointerSpeed this.currentOffset = { x: 0, y: 0 } this.startCoords = interaction.coords.cur.page this.modifierArg = modification.fillArg({ pageCoords: this.startCoords, preEnd: true, phase: 'inertiastart', }) const thrown = this.t0 - interaction.coords.cur.timeStamp < 50 && pointerSpeed > options.minSpeed && pointerSpeed > options.endSpeed if (thrown) { this.startInertia() } else { modification.result = modification.setAll(this.modifierArg) if (!modification.result.changed) { return false } this.startSmoothEnd() } // force modification change interaction.modification.result.rect = null // bring inertiastart event to the target coords interaction.offsetBy(this.targetOffset) interaction._doPhase({ interaction, event, phase: 'inertiastart', }) interaction.offsetBy({ x: -this.targetOffset.x, y: -this.targetOffset.y }) // force modification change interaction.modification.result.rect = null this.active = true interaction.simulation = this return true } startInertia() { const startVelocity = this.interaction.coords.velocity.client const options = getOptions(this.interaction) const lambda = options.resistance const inertiaDur = -Math.log(options.endSpeed / this.v0) / lambda this.targetOffset = { x: (startVelocity.x - inertiaDur) / lambda, y: (startVelocity.y - inertiaDur) / lambda, } this.te = inertiaDur this.lambda_v0 = lambda / this.v0 this.one_ve_v0 = 1 - options.endSpeed / this.v0 const { modification, modifierArg } = this modifierArg.pageCoords = { x: this.startCoords.x + this.targetOffset.x, y: this.startCoords.y + this.targetOffset.y, } modification.result = modification.setAll(modifierArg) if (modification.result.changed) { this.isModified = true this.modifiedOffset = { x: this.targetOffset.x + modification.result.delta.x, y: this.targetOffset.y + modification.result.delta.y, } } this.onNextFrame(() => this.inertiaTick()) } startSmoothEnd() { this.smoothEnd = true this.isModified = true this.targetOffset = { x: this.modification.result.delta.x, y: this.modification.result.delta.y, } this.onNextFrame(() => this.smoothEndTick()) } onNextFrame(tickFn: () => void) { this.timeout = raf.request(() => { if (this.active) { tickFn() } }) } inertiaTick() { const { interaction } = this const options = getOptions(interaction) const lambda = options.resistance const t = (interaction._now() - this.t0) / 1000 if (t < this.te) { const progress = 1 - (Math.exp(-lambda * t) - this.lambda_v0) / this.one_ve_v0 let newOffset: Point if (this.isModified) { newOffset = getQuadraticCurvePoint( 0, 0, this.targetOffset.x, this.targetOffset.y, this.modifiedOffset.x, this.modifiedOffset.y, progress, ) } else { newOffset = { x: this.targetOffset.x * progress, y: this.targetOffset.y * progress, } } const delta = { x: newOffset.x - this.currentOffset.x, y: newOffset.y - this.currentOffset.y } this.currentOffset.x += delta.x this.currentOffset.y += delta.y interaction.offsetBy(delta) interaction.move() this.onNextFrame(() => this.inertiaTick()) } else { interaction.offsetBy({ x: this.modifiedOffset.x - this.currentOffset.x, y: this.modifiedOffset.y - this.currentOffset.y, }) this.end() } } smoothEndTick() { const { interaction } = this const t = interaction._now() - this.t0 const { smoothEndDuration: duration } = getOptions(interaction) if (t < duration) { const newOffset = { x: easeOutQuad(t, 0, this.targetOffset.x, duration), y: easeOutQuad(t, 0, this.targetOffset.y, duration), } const delta = { x: newOffset.x - this.currentOffset.x, y: newOffset.y - this.currentOffset.y, } this.currentOffset.x += delta.x this.currentOffset.y += delta.y interaction.offsetBy(delta) interaction.move({ skipModifiers: this.modifierCount }) this.onNextFrame(() => this.smoothEndTick()) } else { interaction.offsetBy({ x: this.targetOffset.x - this.currentOffset.x, y: this.targetOffset.y - this.currentOffset.y, }) this.end() } } resume({ pointer, event, eventTarget }: SignalArgs['interactions:down']) { const { interaction } = this // undo inertia changes to interaction coords interaction.offsetBy({ x: -this.currentOffset.x, y: -this.currentOffset.y, }) // update pointer at pointer down position interaction.updatePointer(pointer, event, eventTarget, true) // fire resume signals and event interaction._doPhase({ interaction, event, phase: 'resume', }) copyCoords(interaction.coords.prev, interaction.coords.cur) this.stop() } end() { this.interaction.move() this.interaction.end() this.stop() } stop() { this.active = this.smoothEnd = false this.interaction.simulation = null raf.cancel(this.timeout) } } function start({ interaction, event }: DoPhaseArg) { if (!interaction._interacting || interaction.simulation) { return null } const started = interaction.inertia.start(event) // prevent action end if inertia or smoothEnd return started ? false : null } // Check if the down event hits the current inertia target // control should be return to the user function resume(arg: SignalArgs['interactions:down']) { const { interaction, eventTarget } = arg const state = interaction.inertia if (!state.active) return let element = eventTarget as Node // climb up the DOM tree from the event target while (is.element(element)) { // if interaction element is the current inertia target element if (element === interaction.element) { state.resume(arg) break } element = dom.parentNode(element) } } function stop({ interaction }: { interaction: Interaction }) { const state = interaction.inertia if (state.active) { state.stop() } } function getOptions({ interactable, prepared }: Interaction) { return interactable && interactable.options && prepared.name && interactable.options[prepared.name].inertia } const inertia: Plugin = { id: 'inertia', before: ['modifiers', 'actions'], install, listeners: { 'interactions:new': ({ interaction }) => { interaction.inertia = new InertiaState(interaction) }, 'interactions:before-action-end': start, 'interactions:down': resume, 'interactions:stop': stop, 'interactions:before-action-resume': (arg) => { const { modification } = arg.interaction modification.stop(arg) modification.start(arg, arg.interaction.coords.cur.page) modification.applyToInteraction(arg) }, 'interactions:before-action-inertiastart': (arg) => arg.interaction.modification.setAndApply(arg), 'interactions:action-resume': modifiers.addEventModifiers, 'interactions:action-inertiastart': modifiers.addEventModifiers, 'interactions:after-action-inertiastart': (arg) => arg.interaction.modification.restoreInteractionCoords(arg), 'interactions:after-action-resume': (arg) => arg.interaction.modification.restoreInteractionCoords(arg), }, } // http://stackoverflow.com/a/5634528/2280888 function _getQBezierValue(t: number, p1: number, p2: number, p3: number) { const iT = 1 - t return iT * iT * p1 + 2 * iT * t * p2 + t * t * p3 } function getQuadraticCurvePoint( startX: number, startY: number, cpX: number, cpY: number, endX: number, endY: number, position: number, ) { return { x: _getQBezierValue(position, startX, cpX, endX), y: _getQBezierValue(position, startY, cpY, endY), } } // http://gizma.com/easing/ function easeOutQuad(t: number, b: number, c: number, d: number) { t /= d return -c * t * (t - 2) + b } export default inertia ================================================ FILE: packages/@interactjs/interact/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/interact/index.ts ================================================ import { Scope } from '@interactjs/core/scope' const scope = new Scope() const interact = scope.interactStatic export default interact const _global = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this scope.init(_global) ================================================ FILE: packages/@interactjs/interact/interact.spec.ts ================================================ import { Scope } from '@interactjs/core/scope' const makeIframeDoc = () => { const iframe = document.body.appendChild(document.createElement('iframe')) return iframe.contentWindow.document } test('interact export', () => { const scope = new Scope() const interact = scope.interactStatic scope.init(window) const interactable1 = interact('selector') // interact function returns Interactable instance expect(interactable1).toBeInstanceOf(scope.Interactable) // same interactable is returned with same target and context expect(interact('selector')).toBe(interactable1) // new interactables are added to list expect(scope.interactables.list).toHaveLength(1) interactable1.unset() // unset interactables are removed expect(scope.interactables.list).toHaveLength(0) // unset interactions are removed expect(scope.interactions.list).toHaveLength(0) const doc1 = document const doc2 = makeIframeDoc() const results = ( [ ['repeat', doc1], ['repeat', doc2], [doc1, doc1], [doc2.body, doc2], ] as const ).reduce((acc, [target, context]) => { const interactable = interact(target, { context }) // unique contexts make unique interactables with identical targets expect(acc.some((e) => e.interactable === interactable)).toBe(false) acc.push({ interactable, target, context }) return acc }, []) for (const { interactable, target, context } of results) { // interactions.get returns correct result with identical targets and different contexts expect(scope.interactables.getExisting(target, { context })).toBe(interactable) } const doc3 = makeIframeDoc() const prevDocCount = scope.documents.length interact.addDocument(doc3, { events: { passive: false } }) // interact.addDocument() adds to scope with options expect(scope.documents[prevDocCount]).toEqual({ doc: doc3, options: { events: { passive: false } } }) interact.removeDocument(doc3) // interact.removeDocument() removes document from scope expect(scope.documents).toHaveLength(prevDocCount) scope.interactables.list.forEach((i) => i.unset()) const plugin1 = { id: 'test-1', install() { plugin1.count++ }, count: 0, } const plugin2 = { id: '', install() { plugin2.count++ }, count: 0, } interact.use(plugin1) interact.use(plugin2) // new plugin install methods are called expect([plugin1.count, plugin2.count]).toEqual([1, 1]) interact.use({ ...plugin1 }) // different plugin object with same id not installed expect([plugin1.count, plugin2.count]).toEqual([1, 1]) interact.use(plugin2) // plugin without id not re-installed expect([plugin1.count, plugin2.count]).toEqual([1, 1]) }) ================================================ FILE: packages/@interactjs/interact/package.json ================================================ { "name": "@interactjs/interact", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/interact" }, "dependencies": { "@interactjs/core": "1.10.27", "@interactjs/utils": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": false, "license": "MIT" } ================================================ FILE: packages/@interactjs/interactjs/index.stub.ts ================================================ /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import '@interactjs/actions/plugin' import '@interactjs/auto-scroll/plugin' import '@interactjs/auto-start/plugin' import '@interactjs/core/interactablePreventDefault' import '@interactjs/dev-tools/plugin' import '@interactjs/inertia/plugin' import '@interactjs/interact' import '@interactjs/modifiers/plugin' import '@interactjs/offset/plugin' import '@interactjs/pointer-events/plugin' import '@interactjs/reflow/plugin' import actions from '@interactjs/actions/plugin' import autoScroll from '@interactjs/auto-scroll/plugin' import autoStart from '@interactjs/auto-start/plugin' import interactablePreventDefault from '@interactjs/core/interactablePreventDefault' import devTools from '@interactjs/dev-tools/plugin' import inertia from '@interactjs/inertia/plugin' import interact from '@interactjs/interact' import modifiers from '@interactjs/modifiers/plugin' import offset from '@interactjs/offset/plugin' import pointerEvents from '@interactjs/pointer-events/plugin' import reflow from '@interactjs/reflow/plugin' /* eslint-enable import/no-duplicates */ interact.use(interactablePreventDefault) interact.use(offset) // pointerEvents interact.use(pointerEvents) // inertia interact.use(inertia) // snap, resize, etc. interact.use(modifiers) // autoStart, hold interact.use(autoStart) // drag and drop, resize, gesture interact.use(actions) // autoScroll interact.use(autoScroll) // reflow interact.use(reflow) // eslint-disable-next-line no-undef if (process.env.NODE_ENV !== 'production') { interact.use(devTools) } export default interact ;(interact as any).default = interact ================================================ FILE: packages/@interactjs/interactjs/index.ts ================================================ /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import '@interactjs/actions/plugin' import '@interactjs/auto-scroll/plugin' import '@interactjs/auto-start/plugin' import '@interactjs/core/interactablePreventDefault' import '@interactjs/dev-tools/plugin' import '@interactjs/inertia/plugin' import '@interactjs/interact' import '@interactjs/modifiers/plugin' import '@interactjs/offset/plugin' import '@interactjs/pointer-events/plugin' import '@interactjs/reflow/plugin' import actions from '@interactjs/actions/plugin' import autoScroll from '@interactjs/auto-scroll/plugin' import autoStart from '@interactjs/auto-start/plugin' import interactablePreventDefault from '@interactjs/core/interactablePreventDefault' import devTools from '@interactjs/dev-tools/plugin' import inertia from '@interactjs/inertia/plugin' import interact from '@interactjs/interact' import modifiers from '@interactjs/modifiers/plugin' import offset from '@interactjs/offset/plugin' import pointerEvents from '@interactjs/pointer-events/plugin' import reflow from '@interactjs/reflow/plugin' /* eslint-enable import/no-duplicates */ interact.use(interactablePreventDefault) interact.use(offset) // pointerEvents interact.use(pointerEvents) // inertia interact.use(inertia) // snap, resize, etc. interact.use(modifiers) // autoStart, hold interact.use(autoStart) // drag and drop, resize, gesture interact.use(actions) // autoScroll interact.use(autoScroll) // reflow interact.use(reflow) // eslint-disable-next-line no-undef if (process.env.NODE_ENV !== 'production') { interact.use(devTools) } export default interact ;(interact as any).default = interact ================================================ FILE: packages/@interactjs/interactjs/package.json ================================================ { "name": "@interactjs/interactjs", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git" }, "dependencies": { "@interactjs/actions": "1.10.27", "@interactjs/arrange": "1.10.27", "@interactjs/auto-scroll": "1.10.27", "@interactjs/auto-start": "1.10.27", "@interactjs/clone": "1.10.27", "@interactjs/core": "1.10.27", "@interactjs/dev-tools": "1.10.27", "@interactjs/feedback": "1.10.27", "@interactjs/inertia": "1.10.27", "@interactjs/interact": "1.10.27", "@interactjs/modifiers": "1.10.27", "@interactjs/multi-target": "1.10.27", "@interactjs/offset": "1.10.27", "@interactjs/pointer-events": "1.10.27", "@interactjs/react": "1.10.27", "@interactjs/rebound": "1.10.27", "@interactjs/symbol-tree": "1.10.27", "@interactjs/reflow": "1.10.27", "@interactjs/utils": "1.10.27", "@interactjs/vue": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": false, "license": "MIT" } ================================================ FILE: packages/@interactjs/modifiers/Modification.ts ================================================ import type { EventPhase } from '@interactjs/core/InteractEvent' import type { Interaction, DoAnyPhaseArg } from '@interactjs/core/Interaction' import type { EdgeOptions, FullRect, Point, Rect } from '@interactjs/core/types' import clone from '@interactjs/utils/clone' import extend from '@interactjs/utils/extend' import * as rectUtils from '@interactjs/utils/rect' import type { Modifier, ModifierArg, ModifierState } from './types' export interface ModificationResult { delta: Point rectDelta: Rect coords: Point rect: FullRect eventProps: any[] changed: boolean } interface MethodArg { phase: EventPhase pageCoords: Point rect: FullRect coords: Point preEnd?: boolean skipModifiers?: number } export class Modification { states: ModifierState[] = [] startOffset: Rect = { left: 0, right: 0, top: 0, bottom: 0 } startDelta!: Point result!: ModificationResult endResult!: Point startEdges!: EdgeOptions edges: EdgeOptions readonly interaction: Readonly constructor(interaction: Interaction) { this.interaction = interaction this.result = createResult() this.edges = { left: false, right: false, top: false, bottom: false, } } start({ phase }: { phase: EventPhase }, pageCoords: Point) { const { interaction } = this const modifierList = getModifierList(interaction) this.prepareStates(modifierList) this.startEdges = extend({}, interaction.edges) this.edges = extend({}, this.startEdges) this.startOffset = getRectOffset(interaction.rect, pageCoords) this.startDelta = { x: 0, y: 0 } const arg = this.fillArg({ phase, pageCoords, preEnd: false, }) this.result = createResult() this.startAll(arg) const result = (this.result = this.setAll(arg)) return result } fillArg(arg: Partial) { const { interaction } = this arg.interaction = interaction arg.interactable = interaction.interactable arg.element = interaction.element arg.rect ||= interaction.rect arg.edges ||= this.startEdges arg.startOffset = this.startOffset return arg as ModifierArg } startAll(arg: MethodArg & Partial) { for (const state of this.states) { if (state.methods.start) { arg.state = state state.methods.start(arg as ModifierArg) } } } setAll(arg: MethodArg & Partial): ModificationResult { const { phase, preEnd, skipModifiers, rect: unmodifiedRect, edges: unmodifiedEdges } = arg arg.coords = extend({}, arg.pageCoords) arg.rect = extend({}, unmodifiedRect) arg.edges = extend({}, unmodifiedEdges) const states = skipModifiers ? this.states.slice(skipModifiers) : this.states const newResult = createResult(arg.coords, arg.rect) for (const state of states) { const { options } = state const lastModifierCoords = extend({}, arg.coords) let returnValue = null if (state.methods?.set && this.shouldDo(options, preEnd, phase)) { arg.state = state returnValue = state.methods.set(arg as ModifierArg) rectUtils.addEdges(arg.edges, arg.rect, { x: arg.coords.x - lastModifierCoords.x, y: arg.coords.y - lastModifierCoords.y, }) } newResult.eventProps.push(returnValue) } extend(this.edges, arg.edges) newResult.delta.x = arg.coords.x - arg.pageCoords.x newResult.delta.y = arg.coords.y - arg.pageCoords.y newResult.rectDelta.left = arg.rect.left - unmodifiedRect.left newResult.rectDelta.right = arg.rect.right - unmodifiedRect.right newResult.rectDelta.top = arg.rect.top - unmodifiedRect.top newResult.rectDelta.bottom = arg.rect.bottom - unmodifiedRect.bottom const prevCoords = this.result.coords const prevRect = this.result.rect if (prevCoords && prevRect) { const rectChanged = newResult.rect.left !== prevRect.left || newResult.rect.right !== prevRect.right || newResult.rect.top !== prevRect.top || newResult.rect.bottom !== prevRect.bottom newResult.changed = rectChanged || prevCoords.x !== newResult.coords.x || prevCoords.y !== newResult.coords.y } return newResult } applyToInteraction(arg: { phase: EventPhase; rect?: Rect }) { const { interaction } = this const { phase } = arg const curCoords = interaction.coords.cur const startCoords = interaction.coords.start const { result, startDelta } = this const curDelta = result.delta if (phase === 'start') { extend(this.startDelta, result.delta) } for (const [coordsSet, delta] of [ [startCoords, startDelta], [curCoords, curDelta], ] as const) { coordsSet.page.x += delta.x coordsSet.page.y += delta.y coordsSet.client.x += delta.x coordsSet.client.y += delta.y } const { rectDelta } = this.result const rect = arg.rect || interaction.rect rect.left += rectDelta.left rect.right += rectDelta.right rect.top += rectDelta.top rect.bottom += rectDelta.bottom rect.width = rect.right - rect.left rect.height = rect.bottom - rect.top } setAndApply( arg: Partial & { phase: EventPhase preEnd?: boolean skipModifiers?: number modifiedCoords?: Point }, ): void | false { const { interaction } = this const { phase, preEnd, skipModifiers } = arg const result = this.setAll( this.fillArg({ preEnd, phase, pageCoords: arg.modifiedCoords || interaction.coords.cur.page, }), ) this.result = result // don't fire an action move if a modifier would keep the event in the same // cordinates as before if ( !result.changed && (!skipModifiers || skipModifiers < this.states.length) && interaction.interacting() ) { return false } if (arg.modifiedCoords) { const { page } = interaction.coords.cur const adjustment = { x: arg.modifiedCoords.x - page.x, y: arg.modifiedCoords.y - page.y, } result.coords.x += adjustment.x result.coords.y += adjustment.y result.delta.x += adjustment.x result.delta.y += adjustment.y } this.applyToInteraction(arg) } beforeEnd(arg: Omit & { state?: ModifierState }): void | false { const { interaction, event } = arg const states = this.states if (!states || !states.length) { return } let doPreend = false for (const state of states) { arg.state = state const { options, methods } = state const endPosition = methods.beforeEnd && methods.beforeEnd(arg as unknown as ModifierArg) if (endPosition) { this.endResult = endPosition return false } doPreend = doPreend || (!doPreend && this.shouldDo(options, true, arg.phase, true)) } if (doPreend) { // trigger a final modified move before ending interaction.move({ event, preEnd: true }) } } stop(arg: { interaction: Interaction }) { const { interaction } = arg if (!this.states || !this.states.length) { return } const modifierArg: Partial = extend( { states: this.states, interactable: interaction.interactable, element: interaction.element, rect: null, }, arg, ) this.fillArg(modifierArg) for (const state of this.states) { modifierArg.state = state if (state.methods.stop) { state.methods.stop(modifierArg as ModifierArg) } } this.states = null this.endResult = null } prepareStates(modifierList: Modifier[]) { this.states = [] for (let index = 0; index < modifierList.length; index++) { const { options, methods, name } = modifierList[index] this.states.push({ options, methods, index, name, }) } return this.states } restoreInteractionCoords({ interaction: { coords, rect, modification } }: { interaction: Interaction }) { if (!modification.result) return const { startDelta } = modification const { delta: curDelta, rectDelta } = modification.result const coordsAndDeltas = [ [coords.start, startDelta], [coords.cur, curDelta], ] for (const [coordsSet, delta] of coordsAndDeltas as any) { coordsSet.page.x -= delta.x coordsSet.page.y -= delta.y coordsSet.client.x -= delta.x coordsSet.client.y -= delta.y } rect.left -= rectDelta.left rect.right -= rectDelta.right rect.top -= rectDelta.top rect.bottom -= rectDelta.bottom } shouldDo(options, preEnd?: boolean, phase?: string, requireEndOnly?: boolean) { if ( // ignore disabled modifiers !options || options.enabled === false || // check if we require endOnly option to fire move before end (requireEndOnly && !options.endOnly) || // don't apply endOnly modifiers when not ending (options.endOnly && !preEnd) || // check if modifier should run be applied on start (phase === 'start' && !options.setStart) ) { return false } return true } copyFrom(other: Modification) { this.startOffset = other.startOffset this.startDelta = other.startDelta this.startEdges = other.startEdges this.edges = other.edges this.states = other.states.map((s) => clone(s) as ModifierState) this.result = createResult(extend({}, other.result.coords), extend({}, other.result.rect)) } destroy() { for (const prop in this) { this[prop] = null } } } function createResult(coords?: Point, rect?: FullRect): ModificationResult { return { rect, coords, delta: { x: 0, y: 0 }, rectDelta: { left: 0, right: 0, top: 0, bottom: 0, }, eventProps: [], changed: true, } } function getModifierList(interaction) { const actionOptions = interaction.interactable.options[interaction.prepared.name] const actionModifiers = actionOptions.modifiers if (actionModifiers && actionModifiers.length) { return actionModifiers } return ['snap', 'snapSize', 'snapEdges', 'restrict', 'restrictEdges', 'restrictSize'] .map((type) => { const options = actionOptions[type] return ( options && options.enabled && { options, methods: options._methods, } ) }) .filter((m) => !!m) } export function getRectOffset(rect, coords) { return rect ? { left: coords.x - rect.left, top: coords.y - rect.top, right: rect.right - coords.x, bottom: rect.bottom - coords.y, } : { left: 0, top: 0, right: 0, bottom: 0, } } ================================================ FILE: packages/@interactjs/modifiers/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/modifiers/all.ts ================================================ /* eslint-disable n/no-extraneous-import, import/no-unresolved */ import aspectRatio from './aspectRatio' import avoid from './avoid/avoid' import restrictEdges from './restrict/edges' import restrict from './restrict/pointer' import restrictRect from './restrict/rect' import restrictSize from './restrict/size' import rubberband from './rubberband/rubberband' import snapEdges from './snap/edges' import snap from './snap/pointer' import snapSize from './snap/size' import spring from './spring/spring' import transform from './transform/transform' export default { aspectRatio, restrictEdges, restrict, restrictRect, restrictSize, snapEdges, snap, snapSize, spring, avoid, transform, rubberband, } ================================================ FILE: packages/@interactjs/modifiers/aspectRatio.spec.ts ================================================ import resize from '@interactjs/actions/resize/plugin' import * as helpers from '@interactjs/core/tests/_helpers' import type { FullRect, EdgeOptions } from '@interactjs/core/types' import type { AspectRatioOptions } from './aspectRatio' import aspectRatio from './aspectRatio' import modifiersBase from './base' import restrictSize from './restrict/size' const { ltrbwh } = helpers test('modifiers/aspectRatio', () => { const rect = Object.freeze({ left: 0, top: 0, right: 10, bottom: 20, width: 10, height: 20 }) const { interactable, interaction, event, coords, target } = helpers.testEnv({ plugins: [modifiersBase, resize], rect, }) coords.client = coords.page const options: AspectRatioOptions = {} let lastRect: FullRect = null interactable.resizable({ edges: { left: true, top: true, right: true, bottom: true }, modifiers: [aspectRatio(options)], listeners: { move(e) { lastRect = e.rect }, }, }) options.equalDelta = true downStartMoveUp({ x: 2, y: 4.33, edges: { left: true, top: true } }) // `equalDelta: true, 1 { left: true, top: true } expect(lastRect).toEqual(ltrbwh(2, 2, 10, 20, 8, 18)) downStartMoveUp({ x: 30, y: 2, edges: { bottom: true } }) // equalDelta: true, 2, edges: { bottom: true } expect(lastRect).toEqual(ltrbwh(0, 0, 12, 22, 12, 22)) options.equalDelta = false options.ratio = 2 downStartMoveUp({ x: -5, y: 2, edges: { left: true } }) // equalDelta: false, ratio: 2, edges: left expect(lastRect).toEqual(ltrbwh(-5, 12.5, 10, 20, 15, 7.5)) // combine with restrictSize options.modifiers = [ restrictSize({ max: { width: 20, height: 20 }, }), ] options.equalDelta = false options.ratio = 2 downStartMoveUp({ x: 20, y: 0, edges: { right: true } }) // restrictSize with critical primary edge expect(lastRect).toEqual(ltrbwh(0, 0, 20, 10, 20, 10)) downStartMoveUp({ x: 20, y: 20, edges: { bottom: true } }) // restrictSize with critical secondary edge expect(lastRect).toEqual(ltrbwh(0, 0, 20, 10, 20, 10)) options.ratio = 0.5 downStartMoveUp({ x: 5, y: -5, edges: { left: true, bottom: true } }) // equalDelta: false, ratio: 2, edges: left & bottom expect(lastRect).toEqual(ltrbwh(5, 0, 10, 10, 5, 10)) downStartMoveUp({ x: -5, y: -5, edges: { right: true, top: true } }) // equalDelta: false, ratio: 2, edges: right & top expect(lastRect).toEqual(ltrbwh(0, 10, 5, 20, 5, 10)) function downStartMoveUp({ x, y, edges }: { x: number; y: number; edges: EdgeOptions }) { coords.timeStamp = 0 interaction.stop() lastRect = null Object.assign(coords.page, { x: 0, y: 0 }) interaction.pointerDown(event, event, target) interaction.start({ name: 'resize', edges }, interactable, target) Object.assign(coords.page, { x, y }) interaction.pointerMove(event, event, target) interaction.pointerMove(event, event, target) interaction.pointerUp(event, event, target, target) } }) ================================================ FILE: packages/@interactjs/modifiers/aspectRatio.ts ================================================ /** * @module modifiers/aspectRatio * * @description * This modifier forces elements to be resized with a specified dx/dy ratio. * * ```js * interact(target).resizable({ * modifiers: [ * interact.modifiers.snapSize({ * targets: [ interact.snappers.grid({ x: 20, y: 20 }) ], * }), * interact.aspectRatio({ ratio: 'preserve' }), * ], * }); * ``` */ import type { Point, Rect, EdgeOptions } from '@interactjs/core/types' import extend from '@interactjs/utils/extend' import { addEdges } from '@interactjs/utils/rect' import { makeModifier } from './base' import { Modification } from './Modification' import type { Modifier, ModifierModule, ModifierState } from './types' export interface AspectRatioOptions { ratio?: number | 'preserve' equalDelta?: boolean modifiers?: Modifier[] enabled?: boolean } export type AspectRatioState = ModifierState< AspectRatioOptions, { startCoords: Point startRect: Rect linkedEdges: EdgeOptions ratio: number equalDelta: boolean xIsPrimaryAxis: boolean edgeSign: { x: number y: number } subModification: Modification } > const aspectRatio: ModifierModule = { start(arg) { const { state, rect, edges, pageCoords: coords } = arg let { ratio, enabled } = state.options const { equalDelta, modifiers } = state.options if (ratio === 'preserve') { ratio = rect.width / rect.height } state.startCoords = extend({}, coords) state.startRect = extend({}, rect) state.ratio = ratio state.equalDelta = equalDelta const linkedEdges = (state.linkedEdges = { top: edges.top || (edges.left && !edges.bottom), left: edges.left || (edges.top && !edges.right), bottom: edges.bottom || (edges.right && !edges.top), right: edges.right || (edges.bottom && !edges.left), }) state.xIsPrimaryAxis = !!(edges.left || edges.right) if (state.equalDelta) { const sign = (linkedEdges.left ? 1 : -1) * (linkedEdges.top ? 1 : -1) state.edgeSign = { x: sign, y: sign, } } else { state.edgeSign = { x: linkedEdges.left ? -1 : 1, y: linkedEdges.top ? -1 : 1, } } if (enabled !== false) { extend(edges, linkedEdges) } if (!modifiers?.length) return const subModification = new Modification(arg.interaction) subModification.copyFrom(arg.interaction.modification) subModification.prepareStates(modifiers) state.subModification = subModification subModification.startAll({ ...arg }) }, set(arg) { const { state, rect, coords } = arg const { linkedEdges } = state const initialCoords = extend({}, coords) const aspectMethod = state.equalDelta ? setEqualDelta : setRatio extend(arg.edges, linkedEdges) aspectMethod(state, state.xIsPrimaryAxis, coords, rect) if (!state.subModification) { return null } const correctedRect = extend({}, rect) addEdges(linkedEdges, correctedRect, { x: coords.x - initialCoords.x, y: coords.y - initialCoords.y, }) const result = state.subModification.setAll({ ...arg, rect: correctedRect, edges: linkedEdges, pageCoords: coords, prevCoords: coords, prevRect: correctedRect, }) const { delta } = result if (result.changed) { const xIsCriticalAxis = Math.abs(delta.x) > Math.abs(delta.y) // do aspect modification again with critical edge axis as primary aspectMethod(state, xIsCriticalAxis, result.coords, result.rect) extend(coords, result.coords) } return result.eventProps }, defaults: { ratio: 'preserve', equalDelta: false, modifiers: [], enabled: false, }, } function setEqualDelta({ startCoords, edgeSign }: AspectRatioState, xIsPrimaryAxis: boolean, coords: Point) { if (xIsPrimaryAxis) { coords.y = startCoords.y + (coords.x - startCoords.x) * edgeSign.y } else { coords.x = startCoords.x + (coords.y - startCoords.y) * edgeSign.x } } function setRatio( { startRect, startCoords, ratio, edgeSign }: AspectRatioState, xIsPrimaryAxis: boolean, coords: Point, rect: Rect, ) { if (xIsPrimaryAxis) { const newHeight = rect.width / ratio coords.y = startCoords.y + (newHeight - startRect.height) * edgeSign.y } else { const newWidth = rect.height * ratio coords.x = startCoords.x + (newWidth - startRect.width) * edgeSign.x } } export default makeModifier(aspectRatio, 'aspectRatio') export { aspectRatio } ================================================ FILE: packages/@interactjs/modifiers/avoid/avoid.stub.ts ================================================ export { default } from '../noop' ================================================ FILE: packages/@interactjs/modifiers/avoid/avoid.ts ================================================ export { default } from '../noop' ================================================ FILE: packages/@interactjs/modifiers/base.spec.ts ================================================ import * as helpers from '@interactjs/core/tests/_helpers' import type { ActionName, Element } from '@interactjs/core/types' import extend from '@interactjs/utils/extend' import is from '@interactjs/utils/is' import modifiersBase from './base' test('modifiers/base', () => { const { scope, target, interaction, interactable, coords, event } = helpers.testEnv({ plugins: [modifiersBase], }) // modifiers prop is added new Interaction expect(is.object(interaction.modification)).toBe(true) coords.client = coords.page const testAction = { name: 'test' as ActionName } const element = target as Element const startCoords = { x: 100, y: 200 } const moveCoords = { x: 400, y: 500 } const options: any = { target: { x: 100, y: 100 }, setStart: true } let firedEvents: any[] = [] interactable.rectChecker(() => ({ top: 0, left: 0, bottom: 50, right: 50 })) interactable.on('teststart testmove testend', (e) => firedEvents.push(e)) extend(coords.page, startCoords) interaction.pointerDown(event, event, element) ;(interactable.options as any).test = { enabled: true, modifiers: [ { options, methods: targetModifier, }, ], } interaction.start(testAction, interactable, element) // modifier methods.start() was called expect(options.started).toBe(true) // modifier methods.set() was called expect(options.setted).toBe(true) // start event coords are modified expect(interaction.prevEvent.page).toEqual(options.target) // interaction.coords.start are restored after action start phase expect(interaction.coords.start.page).toEqual(startCoords) // interaction.coords.cur are restored after action start phase expect(interaction.coords.cur.page).toEqual(startCoords) extend(coords.page, moveCoords) interaction.pointerMove(event, event, element) // interaction.coords.cur are restored after action move phase expect(interaction.coords.cur.page).toEqual(moveCoords) // interaction.coords.start are restored after action move phase expect(interaction.coords.start.page).toEqual(startCoords) // move event start coords are modified expect({ x: interaction.prevEvent.x0, y: interaction.prevEvent.y0 }).toEqual({ x: 100, y: 100 }) firedEvents = [] scope.interactions.pointerMoveTolerance = 0 interaction.pointerMove(event, event, element) // duplicate result coords are ignored expect(firedEvents).toHaveLength(0) interaction.stop() // modifier methods.stop() was called expect(options.stopped).toBe(true) // don't set start options.setStart = null // add second modifier ;(interactable.options as any).test.modifiers.push({ options, methods: doubleModifier, }) extend(coords.page, startCoords) interaction.pointerDown(event, event, element) interaction.start(testAction, interactable, element) // modifier methods.set() was not called on start phase without options.setStart expect(options.setted).toBeUndefined() // start event coords are not modified without options.setStart expect(interaction.prevEvent.page).toEqual({ x: 100, y: 200 }) // interaction.coords.start are not modified without options.setStart expect(interaction.coords.start.page).toEqual({ x: 100, y: 200 }) extend(coords.page, moveCoords) interaction.pointerMove(event, event, element) // move event coords are modified by all modifiers expect(interaction.prevEvent.page).toEqual({ x: 200, y: 200 }) interaction.pointerMove(event, event, element) expect(() => { interaction._scopeFire('interactions:action-resume', { interaction, phase: 'resume', iEvent: {} as any, event, }) }).not.toThrow() interaction.stop() interaction.pointerUp(event, event, element, element) // interaction coords after stopping are as expected expect(interaction.coords.cur.page).toEqual(moveCoords) }) const targetModifier = { start({ state }: any) { state.options.started = true }, set({ state, coords }: any) { const { target } = state.options coords.x = target.x coords.y = target.y state.options.setted = true }, stop({ state }: any) { state.options.stopped = true delete state.options.started delete state.options.setted }, } const doubleModifier = { start() {}, set({ coords }: any) { coords.x *= 2 coords.y *= 2 }, } ================================================ FILE: packages/@interactjs/modifiers/base.ts ================================================ import type { InteractEvent } from '@interactjs/core/InteractEvent' import type Interaction from '@interactjs/core/Interaction' import type { Plugin } from '@interactjs/core/scope' import { Modification } from './Modification' import type { Modifier, ModifierModule, ModifierState } from './types' declare module '@interactjs/core/Interaction' { interface Interaction { modification?: Modification } } declare module '@interactjs/core/InteractEvent' { interface InteractEvent { modifiers?: Array<{ name: string [key: string]: any }> } } declare module '@interactjs/core/options' { interface PerActionDefaults { modifiers?: Modifier[] } } export function makeModifier< Defaults extends { enabled?: boolean }, State extends ModifierState, Name extends string, Result, >(module: ModifierModule, name?: Name) { const { defaults } = module const methods = { start: module.start, set: module.set, beforeEnd: module.beforeEnd, stop: module.stop, } const modifier = (_options?: Partial) => { const options = (_options || {}) as Defaults options.enabled = options.enabled !== false // add missing defaults to options for (const prop in defaults) { if (!(prop in options)) { ;(options as any)[prop] = defaults[prop] } } const m: Modifier = { options, methods, name, enable: () => { options.enabled = true return m }, disable: () => { options.enabled = false return m }, } return m } if (name && typeof name === 'string') { // for backwrads compatibility modifier._defaults = defaults modifier._methods = methods } return modifier } export function addEventModifiers({ iEvent, interaction, }: { iEvent: InteractEvent interaction: Interaction }) { const result = interaction.modification!.result if (result) { iEvent.modifiers = result.eventProps } } const modifiersBase: Plugin = { id: 'modifiers/base', before: ['actions'], install: (scope) => { scope.defaults.perAction.modifiers = [] }, listeners: { 'interactions:new': ({ interaction }) => { interaction.modification = new Modification(interaction) }, 'interactions:before-action-start': (arg) => { const { interaction } = arg const modification = arg.interaction.modification! modification.start(arg, interaction.coords.start.page) interaction.edges = modification.edges modification.applyToInteraction(arg) }, 'interactions:before-action-move': (arg) => { const { interaction } = arg const { modification } = interaction const ret = modification.setAndApply(arg) interaction.edges = modification.edges return ret }, 'interactions:before-action-end': (arg) => { const { interaction } = arg const { modification } = interaction const ret = modification.beforeEnd(arg) interaction.edges = modification.startEdges return ret }, 'interactions:action-start': addEventModifiers, 'interactions:action-move': addEventModifiers, 'interactions:action-end': addEventModifiers, 'interactions:after-action-start': (arg) => arg.interaction.modification.restoreInteractionCoords(arg), 'interactions:after-action-move': (arg) => arg.interaction.modification.restoreInteractionCoords(arg), 'interactions:stop': (arg) => arg.interaction.modification.stop(arg), }, } export default modifiersBase ================================================ FILE: packages/@interactjs/modifiers/noop.ts ================================================ import type { ModifierFunction } from './types' const noop = (() => {}) as unknown as ModifierFunction noop._defaults = {} export default noop ================================================ FILE: packages/@interactjs/modifiers/package.json ================================================ { "name": "@interactjs/modifiers", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/modifiers" }, "dependencies": { "@interactjs/snappers": "1.10.27" }, "peerDependencies": { "@interactjs/core": "1.10.27", "@interactjs/rebound": "1.10.27", "@interactjs/utils": "1.10.27" }, "optionalDependencies": { "@interactjs/interact": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/modifiers/plugin.ts ================================================ import type { Plugin } from '@interactjs/core/scope' import snappers from '@interactjs/snappers/plugin' /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import './all' import './base' import all from './all' import base from './base' /* eslint-enable import/no-duplicates */ declare module '@interactjs/core/InteractStatic' { export interface InteractStatic { modifiers: typeof all } } const modifiers: Plugin = { id: 'modifiers', install(scope) { const { interactStatic: interact } = scope scope.usePlugin(base) scope.usePlugin(snappers) interact.modifiers = all // for backwrads compatibility for (const type in all) { const { _defaults, _methods } = all[type as keyof typeof all] ;(_defaults as any)._methods = _methods ;(scope.defaults.perAction as any)[type] = _defaults } }, } export default modifiers ================================================ FILE: packages/@interactjs/modifiers/restrict/edges.spec.ts ================================================ import * as helpers from '@interactjs/core/tests/_helpers' import { restrictEdges } from '../restrict/edges' test('restrictEdges', () => { const { interaction } = helpers.testEnv() const edges = { top: true, bottom: true, left: true, right: true } interaction.prepared = {} as any interaction.prepared.edges = edges interaction._rects = {} as any interaction._rects.corrected = { x: 10, y: 20, width: 300, height: 200 } as any interaction._interacting = true const options: any = { enabled: true } const coords = { x: 40, y: 40 } const offset = { top: 0, left: 0, bottom: 0, right: 0 } const state = { options, offset } const arg = { interaction, edges, state } as any arg.coords = { ...coords } // outer restriction options.outer = { top: 100, left: 100, bottom: 200, right: 200 } restrictEdges.set(arg) // outer restriction is applied correctly expect(arg.coords).toEqual({ x: coords.y + 60, y: coords.y + 60 }) arg.coords = { ...coords } // inner restriction options.outer = null options.inner = { top: 0, left: 0, bottom: 10, right: 10 } restrictEdges.set(arg) // inner restriction is applied correctly expect(arg.coords).toEqual({ x: coords.x - 40, y: coords.y - 40 }) // offset Object.assign(offset, { top: 100, left: 100, bottom: 200, right: 200, }) arg.coords = { ...coords } options.outer = { top: 100, left: 100, bottom: 200, right: 200 } options.inner = null restrictEdges.set(arg) // outer restriction is applied correctly with offset expect(arg.coords).toEqual({ x: coords.x + 160, y: coords.x + 160 }) // start interaction.modification = {} as any arg.startOffset = { top: 5, left: 10, bottom: -8, right: -16 } interaction.interactable = { getRect() { return { top: 500, left: 900 } }, } as any options.offset = 'self' restrictEdges.start(arg) // start gets x/y from selector string expect(arg.state.offset).toEqual({ top: 505, left: 910, bottom: 508, right: 916 }) }) ================================================ FILE: packages/@interactjs/modifiers/restrict/edges.ts ================================================ // This modifier adds the options.resize.restrictEdges setting which sets min and // max for the top, left, bottom and right edges of the target being resized. // // interact(target).resize({ // edges: { top: true, left: true }, // restrictEdges: { // inner: { top: 200, left: 200, right: 400, bottom: 400 }, // outer: { top: 0, left: 0, right: 600, bottom: 600 }, // }, // }) import type { Point, Rect } from '@interactjs/core/types' import extend from '@interactjs/utils/extend' import * as rectUtils from '@interactjs/utils/rect' import { makeModifier } from '../base' import type { ModifierArg, ModifierState } from '../types' import type { RestrictOptions } from './pointer' import { getRestrictionRect } from './pointer' export interface RestrictEdgesOptions { inner: RestrictOptions['restriction'] outer: RestrictOptions['restriction'] offset?: RestrictOptions['offset'] endOnly: boolean enabled?: boolean } export type RestrictEdgesState = ModifierState< RestrictEdgesOptions, { inner: Rect outer: Rect offset: RestrictEdgesOptions['offset'] } > const noInner = { top: +Infinity, left: +Infinity, bottom: -Infinity, right: -Infinity } const noOuter = { top: -Infinity, left: -Infinity, bottom: +Infinity, right: +Infinity } function start({ interaction, startOffset, state }: ModifierArg) { const { options } = state let offset: Point if (options) { const offsetRect = getRestrictionRect(options.offset, interaction, interaction.coords.start.page) offset = rectUtils.rectToXY(offsetRect) } offset = offset || { x: 0, y: 0 } state.offset = { top: offset.y + startOffset.top, left: offset.x + startOffset.left, bottom: offset.y - startOffset.bottom, right: offset.x - startOffset.right, } } function set({ coords, edges, interaction, state }: ModifierArg) { const { offset, options } = state if (!edges) { return } const page = extend({}, coords) const inner = getRestrictionRect(options.inner, interaction, page) || ({} as Rect) const outer = getRestrictionRect(options.outer, interaction, page) || ({} as Rect) fixRect(inner, noInner) fixRect(outer, noOuter) if (edges.top) { coords.y = Math.min(Math.max(outer.top + offset.top, page.y), inner.top + offset.top) } else if (edges.bottom) { coords.y = Math.max(Math.min(outer.bottom + offset.bottom, page.y), inner.bottom + offset.bottom) } if (edges.left) { coords.x = Math.min(Math.max(outer.left + offset.left, page.x), inner.left + offset.left) } else if (edges.right) { coords.x = Math.max(Math.min(outer.right + offset.right, page.x), inner.right + offset.right) } } function fixRect(rect: Rect, defaults: Rect) { for (const edge of ['top', 'left', 'bottom', 'right']) { if (!(edge in rect)) { rect[edge] = defaults[edge] } } return rect } const defaults: RestrictEdgesOptions = { inner: null, outer: null, offset: null, endOnly: false, enabled: false, } const restrictEdges = { noInner, noOuter, start, set, defaults, } export default makeModifier(restrictEdges, 'restrictEdges') export { restrictEdges } ================================================ FILE: packages/@interactjs/modifiers/restrict/pointer.spec.ts ================================================ import * as helpers from '@interactjs/core/tests/_helpers' import { restrict } from '../restrict/pointer' test('restrict larger than restriction', () => { const edges = { left: 0, top: 0, right: 200, bottom: 200 } const rect = { ...edges, width: 200, height: 200 } const { interaction } = helpers.testEnv({ rect }) const restriction = { left: 100, top: 50, right: 150, bottom: 150 } const options = { ...restrict.defaults, restriction: null as any, elementRect: { left: 0, top: 0, right: 1, bottom: 1 }, } const state = { options, offset: null as any } const arg: any = { interaction, state, rect, startOffset: rect, coords: { x: 0, y: 0 }, pageCoords: { x: 0, y: 0 }, } options.restriction = () => null as any expect(() => { restrict.start(arg as any) restrict.set(arg as any) }).not.toThrow() options.restriction = restriction restrict.start(arg as any) arg.coords = { x: 0, y: 0 } restrict.set(arg) // allows top and left edge values to be lower than the restriction expect(arg.coords).toEqual({ x: 0, y: 0 }) arg.coords = { x: restriction.left + 10, y: restriction.top + 10 } restrict.set(arg) // keeps the top left edge values lower than the restriction expect(arg.coords).toEqual({ x: restriction.left - rect.left, y: restriction.top - rect.top }) arg.coords = { x: restriction.right - rect.right - 10, y: restriction.bottom - rect.right - 10 } restrict.set(arg) // keeps the bottom right edge values higher than the restriction expect(arg.coords).toEqual({ x: restriction.right - rect.right, y: restriction.bottom - rect.right }) }) ================================================ FILE: packages/@interactjs/modifiers/restrict/pointer.ts ================================================ import type Interaction from '@interactjs/core/Interaction' import type { RectResolvable, Rect, Point } from '@interactjs/core/types' import extend from '@interactjs/utils/extend' import is from '@interactjs/utils/is' import * as rectUtils from '@interactjs/utils/rect' import { makeModifier } from '../base' import type { ModifierArg, ModifierModule, ModifierState } from '../types' export interface RestrictOptions { // where to drag over restriction: RectResolvable<[number, number, Interaction]> // what part of self is allowed to drag over elementRect: Rect offset: Rect // restrict just before the end drag endOnly: boolean enabled?: boolean } export type RestrictState = ModifierState< RestrictOptions, { offset: Rect } > function start({ rect, startOffset, state, interaction, pageCoords }: ModifierArg) { const { options } = state const { elementRect } = options const offset: Rect = extend( { left: 0, top: 0, right: 0, bottom: 0, }, options.offset || {}, ) if (rect && elementRect) { const restriction = getRestrictionRect(options.restriction, interaction, pageCoords) if (restriction) { const widthDiff = restriction.right - restriction.left - rect.width const heightDiff = restriction.bottom - restriction.top - rect.height if (widthDiff < 0) { offset.left += widthDiff offset.right += widthDiff } if (heightDiff < 0) { offset.top += heightDiff offset.bottom += heightDiff } } offset.left += startOffset.left - rect.width * elementRect.left offset.top += startOffset.top - rect.height * elementRect.top offset.right += startOffset.right - rect.width * (1 - elementRect.right) offset.bottom += startOffset.bottom - rect.height * (1 - elementRect.bottom) } state.offset = offset } function set({ coords, interaction, state }: ModifierArg) { const { options, offset } = state const restriction = getRestrictionRect(options.restriction, interaction, coords) if (!restriction) return const rect = rectUtils.xywhToTlbr(restriction) coords.x = Math.max(Math.min(rect.right - offset.right, coords.x), rect.left + offset.left) coords.y = Math.max(Math.min(rect.bottom - offset.bottom, coords.y), rect.top + offset.top) } export function getRestrictionRect( value: RectResolvable<[number, number, Interaction]>, interaction: Interaction, coords?: Point, ) { if (is.func(value)) { return rectUtils.resolveRectLike(value, interaction.interactable, interaction.element, [ coords.x, coords.y, interaction, ]) } else { return rectUtils.resolveRectLike(value, interaction.interactable, interaction.element) } } const defaults: RestrictOptions = { restriction: null, elementRect: null, offset: null, endOnly: false, enabled: false, } const restrict: ModifierModule = { start, set, defaults, } export default makeModifier(restrict, 'restrict') export { restrict } ================================================ FILE: packages/@interactjs/modifiers/restrict/rect.ts ================================================ import extend from '@interactjs/utils/extend' import { makeModifier } from '../base' import { restrict } from './pointer' const defaults = extend( { get elementRect() { return { top: 0, left: 0, bottom: 1, right: 1 } }, set elementRect(_) {}, }, restrict.defaults, ) const restrictRect = { start: restrict.start, set: restrict.set, defaults, } export default makeModifier(restrictRect, 'restrictRect') export { restrictRect } ================================================ FILE: packages/@interactjs/modifiers/restrict/size.spec.ts ================================================ import type { ResizeEvent } from '@interactjs/actions/resize/plugin' import resize from '@interactjs/actions/resize/plugin' import * as helpers from '@interactjs/core/tests/_helpers' import extend from '@interactjs/utils/extend' import * as rectUtils from '@interactjs/utils/rect' import modifiersBase from '../base' import restrictSize from './size' test('restrictSize', () => { const rect = rectUtils.xywhToTlbr({ left: 0, top: 0, right: 200, bottom: 300 }) const { interaction, interactable, coords, down, start, move } = helpers.testEnv({ plugins: [modifiersBase, resize], rect, }) const edges = { left: true, top: true } const action: any = { name: 'resize', edges } const options = { min: { width: 60, height: 50 } as any, max: { width: 300, height: 350 } as any, } let latestEvent: ResizeEvent = null interactable .resizable({ modifiers: [restrictSize(options)], }) .on('resizestart resizemove resizeend', (e) => { latestEvent = e }) down() start(action) extend(coords.page, { x: -50, y: -40 }) move() // within both min and max expect(latestEvent.page).toEqual(coords.page) extend(coords.page, { x: -200, y: -300 }) move() // outside max expect(latestEvent.page).toEqual({ x: -100, y: -50 }) extend(coords.page, { x: 250, y: 320 }) move() // outside min expect(latestEvent.page).toEqual({ x: 140, y: 250 }) // min and max function restrictions let minFuncArgs: any[] let maxFuncArgs: any[] options.min = (...args: any[]) => { minFuncArgs = args } options.max = (...args: any[]) => { maxFuncArgs = args } move() // correct args are passed to min function restriction expect(minFuncArgs).toEqual([coords.page.x, coords.page.y, interaction]) // correct args are passed to max function restriction expect(maxFuncArgs).toEqual([coords.page.x, coords.page.y, interaction]) }) ================================================ FILE: packages/@interactjs/modifiers/restrict/size.ts ================================================ import type { Point, Rect, Size } from '@interactjs/core/types' import extend from '@interactjs/utils/extend' import * as rectUtils from '@interactjs/utils/rect' import { makeModifier } from '../base' import type { ModifierArg, ModifierState } from '../types' import type { RestrictEdgesState } from './edges' import { restrictEdges } from './edges' import type { RestrictOptions } from './pointer' import { getRestrictionRect } from './pointer' const noMin = { width: -Infinity, height: -Infinity } const noMax = { width: +Infinity, height: +Infinity } export interface RestrictSizeOptions { min?: Size | Point | RestrictOptions['restriction'] max?: Size | Point | RestrictOptions['restriction'] endOnly: boolean enabled?: boolean } function start(arg: ModifierArg) { return restrictEdges.start(arg) } export type RestrictSizeState = RestrictEdgesState & ModifierState< RestrictSizeOptions & { inner: Rect; outer: Rect }, { min: Rect max: Rect } > function set(arg: ModifierArg) { const { interaction, state, rect, edges } = arg const { options } = state if (!edges) { return } const minSize = rectUtils.tlbrToXywh(getRestrictionRect(options.min as any, interaction, arg.coords)) || noMin const maxSize = rectUtils.tlbrToXywh(getRestrictionRect(options.max as any, interaction, arg.coords)) || noMax state.options = { endOnly: options.endOnly, inner: extend({}, restrictEdges.noInner), outer: extend({}, restrictEdges.noOuter), } if (edges.top) { state.options.inner.top = rect.bottom - minSize.height state.options.outer.top = rect.bottom - maxSize.height } else if (edges.bottom) { state.options.inner.bottom = rect.top + minSize.height state.options.outer.bottom = rect.top + maxSize.height } if (edges.left) { state.options.inner.left = rect.right - minSize.width state.options.outer.left = rect.right - maxSize.width } else if (edges.right) { state.options.inner.right = rect.left + minSize.width state.options.outer.right = rect.left + maxSize.width } restrictEdges.set(arg) state.options = options } const defaults: RestrictSizeOptions = { min: null, max: null, endOnly: false, enabled: false, } const restrictSize = { start, set, defaults, } export default makeModifier(restrictSize, 'restrictSize') export { restrictSize } ================================================ FILE: packages/@interactjs/modifiers/rubberband/rubberband.stub.ts ================================================ export { default } from '../noop' ================================================ FILE: packages/@interactjs/modifiers/rubberband/rubberband.ts ================================================ export { default } from '../noop' ================================================ FILE: packages/@interactjs/modifiers/snap/edges.spec.ts ================================================ import * as helpers from '@interactjs/core/tests/_helpers' import type { EdgeOptions } from '@interactjs/core/types' import { snapEdges } from '../snap/edges' test('modifiers/snap/edges', () => { const rect = { top: 0, left: 0, bottom: 100, right: 100 } const { interaction, interactable } = helpers.testEnv({ rect }) interaction.interactable = interactable interaction._interacting = true const target0 = Object.freeze({ left: 50, right: 150, top: 0, bottom: 100, }) const options = { targets: [{ ...target0 }], range: Infinity, } const pageCoords = Object.freeze({ x: 0, y: 0 }) const arg = { interaction, // resize from top left edges: { top: true, left: true } as EdgeOptions, interactable: interaction.interactable, state: null as any, pageCoords, coords: { ...pageCoords }, offset: [{ x: 0, y: 0 }], } arg.state = { options } snapEdges.start!(arg as any) snapEdges.set!(arg as any) // modified coords are correct expect(arg.coords).toEqual({ x: target0.left, y: target0.top }) // resize from bottom right arg.edges = { bottom: true, right: true } arg.state = { options } snapEdges.start!(arg as any) snapEdges.set!(arg as any) // modified coord are correct expect(arg.coords).toEqual({ x: target0.right, y: target0.bottom }) }) ================================================ FILE: packages/@interactjs/modifiers/snap/edges.ts ================================================ /** * @module modifiers/snapEdges * * @description * This modifier allows snapping of the edges of targets during resize * interactions. * * ```js * interact(target).resizable({ * snapEdges: { * targets: [interact.snappers.grid({ x: 100, y: 50 })], * }, * }) * * interact(target).resizable({ * snapEdges: { * targets: [ * interact.snappers.grid({ * top: 50, * left: 50, * bottom: 100, * right: 100, * }), * ], * }, * }) * ``` */ import clone from '@interactjs/utils/clone' import extend from '@interactjs/utils/extend' import { makeModifier } from '../base' import type { ModifierArg, ModifierModule } from '../types' import type { SnapOptions, SnapState } from './pointer' import { snapSize } from './size' export type SnapEdgesOptions = Pick function start(arg: ModifierArg) { const { edges } = arg if (!edges) { return null } arg.state.targetFields = arg.state.targetFields || [ [edges.left ? 'left' : 'right', edges.top ? 'top' : 'bottom'], ] return snapSize.start(arg) } const snapEdges: ModifierModule> = { start, set: snapSize.set, defaults: extend(clone(snapSize.defaults), { targets: undefined, range: undefined, offset: { x: 0, y: 0 }, } as const), } export default makeModifier(snapEdges, 'snapEdges') export { snapEdges } ================================================ FILE: packages/@interactjs/modifiers/snap/pointer.spec.ts ================================================ import drag from '@interactjs/actions/drag/plugin' import * as helpers from '@interactjs/core/tests/_helpers' import type { Point } from '@interactjs/core/types' import extend from '@interactjs/utils/extend' import modifiersBase from '../base' import snap from '../snap/pointer' test('modifiers/snap', () => { const rect = helpers.ltrbwh(0, 0, 100, 100, 100, 100) const { interaction, interactable, coords, down, move, start, stop } = helpers.testEnv({ plugins: [modifiersBase, drag], rect, }) coords.client = coords.page const origin = { x: 120, y: 120 } let funcArgs!: { x: number; y: number; offset: number; index: number; unexpected: unknown[] } const target0 = Object.freeze({ x: 50, y: 100 }) const targetFunc = (x, y, _interaction, offset, index, ...unexpected) => { funcArgs = { x, y, offset, index, unexpected } return target0 } const relativePoint = { x: 0, y: 0 } const options = { offset: undefined as Point | undefined, offsetWithOrigin: true, targets: [target0, targetFunc], range: Infinity, relativePoints: [relativePoint], } let lastEventModifiers!: any[] interactable .draggable({ origin, modifiers: [snap(options)], }) .on('dragmove dragstart dragend', (e) => { lastEventModifiers = e.modifiers }) down() start({ name: 'drag' }) extend(coords.page, { x: 50, y: 50 }) move() // event.modifiers entry has expected props expect(Object.keys(lastEventModifiers[0]).sort()).toEqual([ 'delta', 'distance', 'inRange', 'range', 'target', ]) // snaps to target and adds origin which will be subtracted by InteractEvent expect(helpers.getProps(lastEventModifiers[0].target, ['x', 'y'])).toEqual({ x: target0.x + origin.x, y: target0.y + origin.y, }) options.targets = [targetFunc] down() start({ name: 'drag' }) move(true) stop() expect(funcArgs).toEqual({ x: coords.page.x - origin.x, y: coords.page.y - origin.y, offset: { x: origin.x, y: origin.y, relativePoint, index: 0, }, index: 0, // x, y, interaction, offset, index are passed to target function; origin subtracted from x, y unexpected: [], }) options.offset = { x: 300, y: 300 } options.offsetWithOrigin = false down() start({ name: 'drag' }) move(true) const { startOffset } = interaction.modification! const relativeOffset = { x: options.offset.x + startOffset.left, y: options.offset.y + startOffset.top, } // event.modifiers entry has source element of options.targets array, range, and offset expect(helpers.getProps(lastEventModifiers[0].target, ['source', 'range', 'offset'])).toEqual({ source: targetFunc, range: Infinity, offset: { ...relativeOffset, index: 0, relativePoint }, }) // origin not added to target when !options.offsetWithOrigin expect(helpers.getProps(lastEventModifiers[0].target, ['x', 'y'])).toEqual({ x: target0.x + relativeOffset.x, y: target0.y + relativeOffset.y, }) // origin still subtracted from function target x, y args when !options.offsetWithOrigin expect({ x: funcArgs.x, y: funcArgs.y }).toEqual({ x: coords.page.x - origin.x - relativeOffset.x, y: coords.page.y - origin.y - relativeOffset.y, }) }) ================================================ FILE: packages/@interactjs/modifiers/snap/pointer.ts ================================================ import type { Interaction, InteractionProxy } from '@interactjs/core/Interaction' import type { ActionName, Point, RectResolvable, Element } from '@interactjs/core/types' import extend from '@interactjs/utils/extend' import getOriginXY from '@interactjs/utils/getOriginXY' import hypot from '@interactjs/utils/hypot' import is from '@interactjs/utils/is' import { resolveRectLike, rectToXY } from '@interactjs/utils/rect' import { makeModifier } from '../base' import type { ModifierArg, ModifierState } from '../types' export interface Offset { x: number y: number index: number relativePoint?: Point | null } export interface SnapPosition { x?: number y?: number range?: number offset?: Offset [index: string]: any } export type SnapFunction = ( x: number, y: number, interaction: InteractionProxy, offset: Offset, index: number, ) => SnapPosition export type SnapTarget = SnapPosition | SnapFunction export interface SnapOptions { targets?: SnapTarget[] // target range range?: number // self points for snapping. [0,0] = top left, [1,1] = bottom right relativePoints?: Point[] // startCoords = offset snapping from drag start page position offset?: Point | RectResolvable<[Interaction]> | 'startCoords' offsetWithOrigin?: boolean origin?: RectResolvable<[Element]> | Point endOnly?: boolean enabled?: boolean } export type SnapState = ModifierState< SnapOptions, { offsets?: Offset[] closest?: any targetFields?: string[][] } > function start(arg: ModifierArg) { const { interaction, interactable, element, rect, state, startOffset } = arg const { options } = state const origin = options.offsetWithOrigin ? getOrigin(arg) : { x: 0, y: 0 } let snapOffset: Point if (options.offset === 'startCoords') { snapOffset = { x: interaction.coords.start.page.x, y: interaction.coords.start.page.y, } } else { const offsetRect = resolveRectLike(options.offset as any, interactable, element, [interaction]) snapOffset = rectToXY(offsetRect) || { x: 0, y: 0 } snapOffset.x += origin.x snapOffset.y += origin.y } const { relativePoints } = options state.offsets = rect && relativePoints && relativePoints.length ? relativePoints.map((relativePoint, index) => ({ index, relativePoint, x: startOffset.left - rect.width * relativePoint.x + snapOffset.x, y: startOffset.top - rect.height * relativePoint.y + snapOffset.y, })) : [ { index: 0, relativePoint: null, x: snapOffset.x, y: snapOffset.y, }, ] } function set(arg: ModifierArg) { const { interaction, coords, state } = arg const { options, offsets } = state const origin = getOriginXY(interaction.interactable!, interaction.element!, interaction.prepared.name) const page = extend({}, coords) const targets: SnapPosition[] = [] if (!options.offsetWithOrigin) { page.x -= origin.x page.y -= origin.y } for (const offset of offsets!) { const relativeX = page.x - offset.x const relativeY = page.y - offset.y for (let index = 0, len = options.targets!.length; index < len; index++) { const snapTarget = options.targets![index] let target: SnapPosition if (is.func(snapTarget)) { target = snapTarget(relativeX, relativeY, interaction._proxy, offset, index) } else { target = snapTarget } if (!target) { continue } targets.push({ x: (is.number(target.x) ? target.x : relativeX) + offset.x, y: (is.number(target.y) ? target.y : relativeY) + offset.y, range: is.number(target.range) ? target.range : options.range, source: snapTarget, index, offset, }) } } const closest = { target: null, inRange: false, distance: 0, range: 0, delta: { x: 0, y: 0 }, } for (const target of targets) { const range = target.range const dx = target.x - page.x const dy = target.y - page.y const distance = hypot(dx, dy) let inRange = distance <= range // Infinite targets count as being out of range // compared to non infinite ones that are in range if (range === Infinity && closest.inRange && closest.range !== Infinity) { inRange = false } if ( !closest.target || (inRange ? // is the closest target in range? closest.inRange && range !== Infinity ? // the pointer is relatively deeper in this target distance / range < closest.distance / closest.range : // this target has Infinite range and the closest doesn't (range === Infinity && closest.range !== Infinity) || // OR this target is closer that the previous closest distance < closest.distance : // The other is not in range and the pointer is closer to this target !closest.inRange && distance < closest.distance) ) { closest.target = target closest.distance = distance closest.range = range closest.inRange = inRange closest.delta.x = dx closest.delta.y = dy } } if (closest.inRange) { coords.x = closest.target.x coords.y = closest.target.y } state.closest = closest return closest } function getOrigin(arg: Partial>) { const { element } = arg.interaction const optionsOrigin = rectToXY(resolveRectLike(arg.state.options.origin as any, null, null, [element])) const origin = optionsOrigin || getOriginXY(arg.interactable, element, arg.interaction.prepared.name) return origin } const defaults: SnapOptions = { range: Infinity, targets: null, offset: null, offsetWithOrigin: true, origin: null, relativePoints: null, endOnly: false, enabled: false, } const snap = { start, set, defaults, } export default makeModifier(snap, 'snap') export { snap } ================================================ FILE: packages/@interactjs/modifiers/snap/size.spec.ts ================================================ import * as helpers from '@interactjs/core/tests/_helpers' import { snapSize } from '../snap/size' test('modifiers/snapSize', () => { const { interaction, interactable } = helpers.testEnv() interaction.interactable = interactable interactable.getRect = () => ({ top: 0, left: 0, bottom: 100, right: 100 }) as any interaction._interacting = true const target0 = Object.freeze({ x: 50, y: 100 }) const options = { targets: [{ ...target0 }], range: Infinity, } const state = { options, delta: { x: 0, y: 0 }, offset: [{ x: 0, y: 0 }], } const pageCoords = Object.freeze({ x: 10, y: 20 }) const arg = { interaction, interactable: interaction.interactable, edges: { top: true, left: true }, state, pageCoords, coords: { ...pageCoords }, } snapSize.start(arg as any) snapSize.set(arg) // snapSize.set single target, zereo offset expect(arg.coords).toEqual(target0) }) ================================================ FILE: packages/@interactjs/modifiers/snap/size.ts ================================================ // This modifier allows snapping of the size of targets during resize // interactions. import extend from '@interactjs/utils/extend' import is from '@interactjs/utils/is' import { makeModifier } from '../base' import type { ModifierArg } from '../types' import type { SnapOptions, SnapState } from './pointer' import { snap } from './pointer' export type SnapSizeOptions = Pick function start(arg: ModifierArg) { const { state, edges } = arg const { options } = state if (!edges) { return null } arg.state = { options: { targets: null, relativePoints: [ { x: edges.left ? 0 : 1, y: edges.top ? 0 : 1, }, ], offset: options.offset || 'self', origin: { x: 0, y: 0 }, range: options.range, }, } state.targetFields = state.targetFields || [ ['width', 'height'], ['x', 'y'], ] snap.start(arg) state.offsets = arg.state.offsets arg.state = state } function set(arg) { const { interaction, state, coords } = arg const { options, offsets } = state const relative = { x: coords.x - offsets[0].x, y: coords.y - offsets[0].y, } state.options = extend({}, options) state.options.targets = [] for (const snapTarget of options.targets || []) { let target if (is.func(snapTarget)) { target = snapTarget(relative.x, relative.y, interaction) } else { target = snapTarget } if (!target) { continue } for (const [xField, yField] of state.targetFields) { if (xField in target || yField in target) { target.x = target[xField] target.y = target[yField] break } } state.options.targets.push(target) } const returnValue = snap.set(arg) state.options = options return returnValue } const defaults: SnapSizeOptions = { range: Infinity, targets: null, offset: null, endOnly: false, enabled: false, } const snapSize = { start, set, defaults, } export default makeModifier(snapSize, 'snapSize') export { snapSize } ================================================ FILE: packages/@interactjs/modifiers/spring/spring.stub.ts ================================================ export { default } from '../noop' ================================================ FILE: packages/@interactjs/modifiers/spring/spring.ts ================================================ export { default } from '../noop' ================================================ FILE: packages/@interactjs/modifiers/transform/transform.stub.ts ================================================ export { default } from '../noop' ================================================ FILE: packages/@interactjs/modifiers/transform/transform.ts ================================================ export { default } from '../noop' ================================================ FILE: packages/@interactjs/modifiers/types.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { EventPhase } from '@interactjs/core/InteractEvent' import type Interaction from '@interactjs/core/Interaction' import type { EdgeOptions, FullRect, Point, Rect } from '@interactjs/core/types' export interface Modifier< Defaults = any, State extends ModifierState = any, Name extends string = any, Result = any, > { options: Defaults methods: { start?: (arg: ModifierArg) => void set?: (arg: ModifierArg) => Result beforeEnd?: (arg: ModifierArg) => Point | void stop?: (arg: ModifierArg) => void } name?: Name enable: () => Modifier disable: () => Modifier } export type ModifierState = { options: Defaults methods?: Modifier['methods'] index?: number name?: Name } & StateProps export interface ModifierArg { interaction: Interaction interactable: Interactable phase: EventPhase rect: FullRect edges: EdgeOptions state: State element: Element pageCoords: Point prevCoords: Point prevRect?: FullRect coords: Point startOffset: Rect preEnd?: boolean } export interface ModifierModule< Defaults extends { enabled?: boolean }, State extends ModifierState, Result = unknown, > { defaults?: Defaults start?(arg: ModifierArg): void set?(arg: ModifierArg): Result beforeEnd?(arg: ModifierArg): Point | void stop?(arg: ModifierArg): void } export interface ModifierFunction< Defaults extends { enabled?: boolean }, State extends ModifierState, Name extends string, > { (_options?: Partial): Modifier _defaults: Defaults _methods: ModifierModule } ================================================ FILE: packages/@interactjs/offset/offset.spec.ts ================================================ import * as helpers from '@interactjs/core/tests/_helpers' import offset from './plugin' test('plugins/spring', () => { const { interaction, event, coords, target } = helpers.testEnv({ plugins: [offset] }) const body = target as HTMLBodyElement interaction.pointerMove(event, event, body) interaction.offsetBy({ x: 100, y: 100 }) interaction.pointerMove(event, event, body) // coords are not updated when pointer is not down expect(interaction.coords.cur.page).toEqual(coords.page) interaction.pointerUp(event, event, body, body) interaction.stop() interaction.pointerDown(event, event, body) interaction.offsetBy({ x: 100, y: 50 }) interaction.pointerMove(event, event, body) // coords are not updated when pointer is not down expect(interaction.coords.cur.page).toEqual({ x: coords.page.x + 100, y: coords.page.y + 50 }) }) ================================================ FILE: packages/@interactjs/offset/package.json ================================================ { "name": "@interactjs/offset", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/offset" }, "peerDependencies": { "@interactjs/core": "1.10.27", "@interactjs/utils": "1.10.27" }, "optionalDependencies": { "@interactjs/interact": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/offset/plugin.ts ================================================ import type Interaction from '@interactjs/core/Interaction' import { _ProxyMethods } from '@interactjs/core/Interaction' import type { Plugin } from '@interactjs/core/scope' import type { Point } from '@interactjs/core/types' import * as rectUtils from '@interactjs/utils/rect' declare module '@interactjs/core/Interaction' { interface Interaction { offsetBy?: typeof offsetBy offset: { total: Point pending: Point } } enum _ProxyMethods { offsetBy = '', } } ;(_ProxyMethods as any).offsetBy = '' export function addTotal(interaction: Interaction) { if (!interaction.pointerIsDown) { return } addToCoords(interaction.coords.cur, interaction.offset.total) interaction.offset.pending.x = 0 interaction.offset.pending.y = 0 } function beforeAction({ interaction }: { interaction: Interaction }) { applyPending(interaction) } function beforeEnd({ interaction }: { interaction: Interaction }): boolean | void { const hadPending = applyPending(interaction) if (!hadPending) return interaction.move({ offset: true }) interaction.end() return false } function end({ interaction }: { interaction: Interaction }) { interaction.offset.total.x = 0 interaction.offset.total.y = 0 interaction.offset.pending.x = 0 interaction.offset.pending.y = 0 } export function applyPending(interaction: Interaction) { if (!hasPending(interaction)) { return false } const { pending } = interaction.offset addToCoords(interaction.coords.cur, pending) addToCoords(interaction.coords.delta, pending) rectUtils.addEdges(interaction.edges, interaction.rect, pending) pending.x = 0 pending.y = 0 return true } function offsetBy(this: Interaction, { x, y }: Point) { this.offset.pending.x += x this.offset.pending.y += y this.offset.total.x += x this.offset.total.y += y } function addToCoords({ page, client }, { x, y }: Point) { page.x += x page.y += y client.x += x client.y += y } function hasPending(interaction: Interaction) { return !!(interaction.offset.pending.x || interaction.offset.pending.y) } const offset: Plugin = { id: 'offset', before: ['modifiers', 'pointer-events', 'actions', 'inertia'], install(scope) { scope.Interaction.prototype.offsetBy = offsetBy }, listeners: { 'interactions:new': ({ interaction }) => { interaction.offset = { total: { x: 0, y: 0 }, pending: { x: 0, y: 0 }, } }, 'interactions:update-pointer': ({ interaction }) => addTotal(interaction), 'interactions:before-action-start': beforeAction, 'interactions:before-action-move': beforeAction, 'interactions:before-action-end': beforeEnd, 'interactions:stop': end, }, } export default offset ================================================ FILE: packages/@interactjs/pointer-events/PointerEvent.spec.ts ================================================ import * as helpers from '@interactjs/core/tests/_helpers' import * as pointerUtils from '@interactjs/utils/pointerUtils' import { PointerEvent } from './PointerEvent' test('PointerEvent constructor', () => { const type = 'TEST_EVENT' const pointerId = -100 const testPointerProp = ['TEST_POINTER_PROP'] const pointer = { pointerId, testPointerProp, pointerType: 'TEST_POINTER_TYPE', } as any const testEventProp = ['TEST_EVENT_PROP'] const event = { testEventProp, } as any const { interaction } = helpers.testEnv() const eventTarget = {} as Element const pointerEvent = new PointerEvent(type, pointer, event, eventTarget, interaction as any, 0) as any // pointerEvent is extended form pointer expect(pointerEvent.testPointerProp).toBe(testPointerProp) // pointerEvent is extended form Event expect(pointerEvent.testEventProp).toBe(testEventProp) // type is set correctly expect(pointerEvent.type).toBe(type) // pointerType is set correctly expect(pointerEvent.pointerType).toBe(pointerUtils.getPointerType(pointer)) // pointerId is set correctly expect(pointerEvent.pointerId).toBe(pointerId) // originalEvent is set correctly expect(pointerEvent.originalEvent).toBe(event) // interaction is set correctly expect(pointerEvent.interaction).toBe(interaction._proxy) // target is set correctly expect(pointerEvent.target).toBe(eventTarget) // currentTarget is null expect(pointerEvent.currentTarget).toBeNull() }) test('PointerEvent methods', () => { const methodContexts = {} as any const event: any = ['preventDefault', 'stopPropagation', 'stopImmediatePropagation'].reduce( (acc, methodName) => { acc[methodName] = function () { methodContexts[methodName] = this } return acc }, helpers.newPointer(), ) const pointerEvent = new PointerEvent('TEST', {} as any, event, null, {} as any, 0) pointerEvent.preventDefault() // PointerEvent.preventDefault() calls preventDefault of originalEvent expect(methodContexts.preventDefault).toBe(event) // propagationStopped is false before call to stopPropagation expect(pointerEvent.propagationStopped).toBe(false) pointerEvent.stopPropagation() // stopPropagation sets propagationStopped to true expect(pointerEvent.propagationStopped).toBe(true) // PointerEvent.stopPropagation() does not call stopPropagation of originalEvent // immediatePropagationStopped is false before call to stopImmediatePropagation expect(methodContexts.stopPropagation).toBeUndefined() expect(pointerEvent.immediatePropagationStopped).toBe(false) pointerEvent.stopImmediatePropagation() // PointerEvent.stopImmediatePropagation() does not call stopImmediatePropagation of originalEvent expect(methodContexts.stopImmediatePropagation).toBeUndefined() // stopImmediatePropagation sets immediatePropagationStopped to true expect(pointerEvent.immediatePropagationStopped).toBe(true) const origin = { x: 20, y: 30 } pointerEvent._subtractOrigin(origin) // subtractOrigin updates pageX correctly expect(pointerEvent.pageX).toBe(event.pageX - origin.x) // subtractOrigin updates pageY correctly expect(pointerEvent.pageY).toBe(event.pageY - origin.y) // subtractOrigin updates clientX correctly expect(pointerEvent.clientX).toBe(event.clientX - origin.x) // subtractOrigin updates clientY correctly expect(pointerEvent.clientY).toBe(event.clientY - origin.y) pointerEvent._addOrigin(origin) // addOrigin with the subtracted origin reverts to original coordinates expect(['pageX', 'pageY', 'clientX', 'clientY'].every((prop) => pointerEvent[prop] === event[prop])).toBe( true, ) }) ================================================ FILE: packages/@interactjs/pointer-events/PointerEvent.ts ================================================ import { BaseEvent } from '@interactjs/core/BaseEvent' import type Interaction from '@interactjs/core/Interaction' import type { PointerEventType, PointerType, Point } from '@interactjs/core/types' import * as pointerUtils from '@interactjs/utils/pointerUtils' export class PointerEvent extends BaseEvent { declare type: T declare originalEvent: PointerEventType declare pointerId: number declare pointerType: string declare double: boolean declare pageX: number declare pageY: number declare clientX: number declare clientY: number declare dt: number declare eventable: any; [key: string]: any constructor( type: T, pointer: PointerType | PointerEvent, event: PointerEventType, eventTarget: Node, interaction: Interaction, timeStamp: number, ) { super(interaction) pointerUtils.pointerExtend(this, event) if (event !== pointer) { pointerUtils.pointerExtend(this, pointer) } this.timeStamp = timeStamp this.originalEvent = event this.type = type this.pointerId = pointerUtils.getPointerId(pointer) this.pointerType = pointerUtils.getPointerType(pointer) this.target = eventTarget this.currentTarget = null if (type === 'tap') { const pointerIndex = interaction.getPointerIndex(pointer) this.dt = this.timeStamp - interaction.pointers[pointerIndex].downTime const interval = this.timeStamp - interaction.tapTime this.double = !!interaction.prevTap && interaction.prevTap.type !== 'doubletap' && interaction.prevTap.target === this.target && interval < 500 } else if (type === 'doubletap') { this.dt = (pointer as PointerEvent<'tap'>).timeStamp - interaction.tapTime this.double = true } } _subtractOrigin({ x: originX, y: originY }: Point) { this.pageX -= originX this.pageY -= originY this.clientX -= originX this.clientY -= originY return this } _addOrigin({ x: originX, y: originY }: Point) { this.pageX += originX this.pageY += originY this.clientX += originX this.clientY += originY return this } /** * Prevent the default behaviour of the original Event */ preventDefault() { this.originalEvent.preventDefault() } } ================================================ FILE: packages/@interactjs/pointer-events/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/pointer-events/base.spec.ts ================================================ import { Eventable } from '@interactjs/core/Eventable' import type { Scope } from '@interactjs/core/scope' import * as helpers from '@interactjs/core/tests/_helpers' import type { PointerEventType, PointerType } from '@interactjs/core/types' import type { EventTargetList } from './base' import pointerEvents from './base' import interactableTargets from './interactableTargets' test('pointerEvents.types', () => { expect(pointerEvents.types).toEqual({ down: true, move: true, up: true, cancel: true, tap: true, doubletap: true, hold: true, }) }) test('pointerEvents.fire', () => { const { scope, interaction, event, coords } = helpers.testEnv({ plugins: [pointerEvents] }) const eventable = new Eventable(pointerEvents.defaults) const type = 'TEST' const element = {} const eventTarget = {} const TEST_PROP = ['TEST_PROP'] let firedEvent: any const targets: EventTargetList = [ { eventable, node: element as Node, props: { TEST_PROP, }, }, ] eventable.on(type, (e) => { firedEvent = e }) pointerEvents.fire( { type, eventTarget, pointer: {}, event: {}, interaction: {}, targets, } as any, scope, ) // Fired event is an instance of pointerEvents.PointerEvent expect(firedEvent instanceof pointerEvents.PointerEvent).toBe(true) // Fired event type is correct expect(firedEvent.type).toBe(type) // Fired event currentTarget is correct expect(firedEvent.currentTarget).toBe(element) // Fired event target is correct expect(firedEvent.target).toBe(eventTarget) // Fired event has props from target.props expect(firedEvent.TEST_PROP).toBe(TEST_PROP) scope.now = () => coords.timeStamp coords.timeStamp = 0 interaction.pointerDown(event, event, scope.document) coords.timeStamp = 500 interaction.pointerUp(event, event, scope.document, scope.document) // interaction.tapTime is updated expect(interaction.tapTime).toBe(500) // interaction.prevTap is updated expect(interaction.prevTap.type).toBe('tap') }) test('pointerEvents.collectEventTargets', () => { const { scope, interaction } = helpers.testEnv() const type = 'TEST' const TEST_PROP = ['TEST_PROP'] const target = { node: {} as Node, props: { TEST_PROP }, eventable: new Eventable(pointerEvents.defaults), } let collectedTargets: EventTargetList function onCollect({ targets }: { targets?: EventTargetList }) { targets.push(target) collectedTargets = targets } scope.addListeners({ 'pointerEvents:collect-targets': onCollect, }) pointerEvents.collectEventTargets( { interaction, pointer: {}, event: {}, eventTarget: {}, type, } as any, scope, ) expect(collectedTargets).toEqual([target]) }) test('pointerEvents Interaction update-pointer signal', () => { const scope: Scope = helpers.mockScope() scope.usePlugin(pointerEvents) const interaction = scope.interactions.new({}) const initialHold = { duration: Infinity, timeout: null as number } const event = {} as PointerEventType interaction.updatePointer(helpers.newPointer(0), event, null, false) // set hold info for move on new pointer expect(interaction.pointers.map((p) => p.hold)).toEqual([initialHold]) interaction.removePointer(helpers.newPointer(0), event) interaction.updatePointer(helpers.newPointer(0), event, null, true) expect(interaction.pointers.map((p) => p.hold)).toEqual([initialHold]) interaction.updatePointer(helpers.newPointer(5), event, null, true) expect(interaction.pointers.map((p) => p.hold)).toEqual([initialHold, initialHold]) }) test('pointerEvents Interaction remove-pointer signal', () => { const scope: Scope = helpers.mockScope() scope.usePlugin(pointerEvents) const interaction = scope.interactions.new({}) const ids = [0, 1, 2, 3] const removals = [ { id: 0, remain: [1, 2, 3], message: 'first of 4' }, { id: 2, remain: [1, 3], message: 'middle of 3' }, { id: 3, remain: [1], message: 'last of 2' }, { id: 1, remain: [], message: 'final' }, ] for (const id of ids) { const index = interaction.updatePointer( { pointerId: id } as PointerType, {} as PointerEventType, null, true, ) // use the ids as the pointerInfo.hold value for this test interaction.pointers[index].hold = id as any } for (const removal of removals) { interaction.removePointer({ pointerId: removal.id } as any, null) // `${removal.message} - remaining interaction.pointers[i].hold are correct` expect(interaction.pointers.map((p) => p.hold as unknown as number)).toEqual(removal.remain) } }) test('pointerEvents down hold up tap', async () => { const { interaction, event, interactable } = helpers.testEnv({ plugins: [pointerEvents, interactableTargets], }) const fired: PointerEvent[] = [] for (const type in pointerEvents.types) { interactable.on(type, (e) => fired.push(e)) } interaction.pointerDown(event, event, event.target) interaction.pointerMove(event, event, event.target) // duplicate move event is not fired expect(fired.map((e) => e.type)).toEqual(['down']) const holdTimer = interaction.pointers[0].hold // hold timeout is set expect(holdTimer.timeout).toBeTruthy() await helpers.timeout(holdTimer.duration) interaction.pointerUp(event, event, event.target, event.target) // tap event is fired after down, hold and up events expect(fired.map((e) => e.type)).toEqual(['down', 'hold', 'up', 'tap']) }) ================================================ FILE: packages/@interactjs/pointer-events/base.ts ================================================ import type { Eventable } from '@interactjs/core/Eventable' import type { Interaction } from '@interactjs/core/Interaction' import type { PerActionDefaults } from '@interactjs/core/options' import type { Scope, SignalArgs, Plugin } from '@interactjs/core/scope' import type { Point, PointerType, PointerEventType, Element } from '@interactjs/core/types' import * as domUtils from '@interactjs/utils/domUtils' import extend from '@interactjs/utils/extend' import getOriginXY from '@interactjs/utils/getOriginXY' import { PointerEvent } from './PointerEvent' export type EventTargetList = Array<{ node: Node eventable: Eventable props: { [key: string]: any } }> export interface PointerEventOptions extends PerActionDefaults { enabled?: undefined // not used holdDuration?: number ignoreFrom?: any allowFrom?: any origin?: Point | string | Element } declare module '@interactjs/core/scope' { interface Scope { pointerEvents: typeof pointerEvents } } declare module '@interactjs/core/Interaction' { interface Interaction { prevTap?: PointerEvent tapTime?: number } } declare module '@interactjs/core/PointerInfo' { interface PointerInfo { hold?: { duration: number timeout: any } } } declare module '@interactjs/core/options' { interface ActionDefaults { pointerEvents: Options } } declare module '@interactjs/core/scope' { interface SignalArgs { 'pointerEvents:new': { pointerEvent: PointerEvent } 'pointerEvents:fired': { interaction: Interaction pointer: PointerType | PointerEvent event: PointerEventType | PointerEvent eventTarget: Node pointerEvent: PointerEvent targets?: EventTargetList type: string } 'pointerEvents:collect-targets': { interaction: Interaction pointer: PointerType | PointerEvent event: PointerEventType | PointerEvent eventTarget: Node targets?: EventTargetList type: string path: Node[] node: null } } } const defaults: PointerEventOptions = { holdDuration: 600, ignoreFrom: null, allowFrom: null, origin: { x: 0, y: 0 }, } const pointerEvents: Plugin = { id: 'pointer-events/base', before: ['inertia', 'modifiers', 'auto-start', 'actions'], install, listeners: { 'interactions:new': addInteractionProps, 'interactions:update-pointer': addHoldInfo, 'interactions:move': moveAndClearHold, 'interactions:down': (arg, scope) => { downAndStartHold(arg, scope) fire(arg, scope) }, 'interactions:up': (arg, scope) => { clearHold(arg) fire(arg, scope) tapAfterUp(arg, scope) }, 'interactions:cancel': (arg, scope) => { clearHold(arg) fire(arg, scope) }, }, PointerEvent, fire, collectEventTargets, defaults, types: { down: true, move: true, up: true, cancel: true, tap: true, doubletap: true, hold: true, } as { [type: string]: true }, } function fire( arg: { pointer: PointerType | PointerEvent event: PointerEventType | PointerEvent eventTarget: Node interaction: Interaction type: T targets?: EventTargetList }, scope: Scope, ) { const { interaction, pointer, event, eventTarget, type, targets = collectEventTargets(arg, scope) } = arg const pointerEvent = new PointerEvent(type, pointer, event, eventTarget, interaction, scope.now()) scope.fire('pointerEvents:new', { pointerEvent }) const signalArg = { interaction, pointer, event, eventTarget, targets, type, pointerEvent, } for (let i = 0; i < targets.length; i++) { const target = targets[i] for (const prop in target.props || {}) { ;(pointerEvent as any)[prop] = target.props[prop] } const origin = getOriginXY(target.eventable, target.node) pointerEvent._subtractOrigin(origin) pointerEvent.eventable = target.eventable pointerEvent.currentTarget = target.node target.eventable.fire(pointerEvent) pointerEvent._addOrigin(origin) if ( pointerEvent.immediatePropagationStopped || (pointerEvent.propagationStopped && i + 1 < targets.length && targets[i + 1].node !== pointerEvent.currentTarget) ) { break } } scope.fire('pointerEvents:fired', signalArg) if (type === 'tap') { // if pointerEvent should make a double tap, create and fire a doubletap // PointerEvent and use that as the prevTap const prevTap = pointerEvent.double ? fire( { interaction, pointer, event, eventTarget, type: 'doubletap', }, scope, ) : pointerEvent interaction.prevTap = prevTap interaction.tapTime = prevTap.timeStamp } return pointerEvent } function collectEventTargets( { interaction, pointer, event, eventTarget, type, }: { interaction: Interaction pointer: PointerType | PointerEvent event: PointerEventType | PointerEvent eventTarget: Node type: T }, scope: Scope, ) { const pointerIndex = interaction.getPointerIndex(pointer) const pointerInfo = interaction.pointers[pointerIndex] // do not fire a tap event if the pointer was moved before being lifted if ( type === 'tap' && (interaction.pointerWasMoved || // or if the pointerup target is different to the pointerdown target !(pointerInfo && pointerInfo.downTarget === eventTarget)) ) { return [] } const path = domUtils.getPath(eventTarget as Element | Document) const signalArg = { interaction, pointer, event, eventTarget, type, path, targets: [] as EventTargetList, node: null, } for (const node of path) { signalArg.node = node scope.fire('pointerEvents:collect-targets', signalArg) } if (type === 'hold') { signalArg.targets = signalArg.targets.filter( (target) => target.eventable.options.holdDuration === interaction.pointers[pointerIndex]?.hold?.duration, ) } return signalArg.targets } function addInteractionProps({ interaction }) { interaction.prevTap = null // the most recent tap event on this interaction interaction.tapTime = 0 // time of the most recent tap event } function addHoldInfo({ down, pointerInfo }: SignalArgs['interactions:update-pointer']) { if (!down && pointerInfo.hold) { return } pointerInfo.hold = { duration: Infinity, timeout: null } } function clearHold({ interaction, pointerIndex }) { const hold = interaction.pointers[pointerIndex].hold if (hold && hold.timeout) { clearTimeout(hold.timeout) hold.timeout = null } } function moveAndClearHold(arg: SignalArgs['interactions:move'], scope: Scope) { const { interaction, pointer, event, eventTarget, duplicate } = arg if (!duplicate && (!interaction.pointerIsDown || interaction.pointerWasMoved)) { if (interaction.pointerIsDown) { clearHold(arg) } fire( { interaction, pointer, event, eventTarget: eventTarget as Element, type: 'move', }, scope, ) } } function downAndStartHold( { interaction, pointer, event, eventTarget, pointerIndex }: SignalArgs['interactions:down'], scope: Scope, ) { const timer = interaction.pointers[pointerIndex].hold! const path = domUtils.getPath(eventTarget as Element | Document) const signalArg = { interaction, pointer, event, eventTarget, type: 'hold', targets: [] as EventTargetList, path, node: null, } for (const node of path) { signalArg.node = node scope.fire('pointerEvents:collect-targets', signalArg) } if (!signalArg.targets.length) return let minDuration = Infinity for (const target of signalArg.targets) { const holdDuration = target.eventable.options.holdDuration if (holdDuration < minDuration) { minDuration = holdDuration } } timer.duration = minDuration timer.timeout = setTimeout(() => { fire( { interaction, eventTarget, pointer, event, type: 'hold', }, scope, ) }, minDuration) } function tapAfterUp( { interaction, pointer, event, eventTarget }: SignalArgs['interactions:up'], scope: Scope, ) { if (!interaction.pointerWasMoved) { fire({ interaction, eventTarget, pointer, event, type: 'tap' }, scope) } } function install(scope: Scope) { scope.pointerEvents = pointerEvents scope.defaults.actions.pointerEvents = pointerEvents.defaults extend(scope.actions.phaselessTypes, pointerEvents.types) } export default pointerEvents ================================================ FILE: packages/@interactjs/pointer-events/holdRepeat.spec.ts ================================================ import { Eventable } from '@interactjs/core/Eventable' import * as helpers from '@interactjs/core/tests/_helpers' import holdRepeat from './holdRepeat' test('holdRepeat count', () => { const pointerEvent = { type: 'hold', count: 0, } const { scope } = helpers.testEnv({ plugins: [holdRepeat] }) scope.fire('pointerEvents:new', { pointerEvent } as any) // first hold count is 1 with count previously undefined expect(pointerEvent.count).toBe(1) const count = 20 pointerEvent.count = count scope.fire('pointerEvents:new', { pointerEvent } as any) // existing hold count is incremented expect(pointerEvent.count).toBe(count + 1) }) test('holdRepeat onFired', () => { const { scope, interaction } = helpers.testEnv({ plugins: [holdRepeat] }) const pointerEvent = { type: 'hold', } const eventTarget = {} const eventable = new Eventable( Object.assign({}, scope.pointerEvents.defaults, { holdRepeatInterval: 0, }), ) const signalArg = { interaction, pointerEvent, eventTarget, targets: [ { eventable, }, ], } scope.fire('pointerEvents:fired', signalArg as any) // interaction interval handle was not saved with 0 holdRepeatInterval expect('holdIntervalHandle' in interaction).toBe(false) eventable.options.holdRepeatInterval = 10 scope.fire('pointerEvents:fired', signalArg as any) // interaction interval handle was saved with interval > 0 expect('holdIntervalHandle' in interaction).toBe(true) clearInterval(interaction.holdIntervalHandle) pointerEvent.type = 'NOT_HOLD' delete interaction.holdIntervalHandle scope.fire('pointerEvents:fired', signalArg as any) // interaction interval handle is not saved if pointerEvent.type is not "hold" expect('holdIntervalHandle' in interaction).toBe(false) }) ================================================ FILE: packages/@interactjs/pointer-events/holdRepeat.ts ================================================ import type Interaction from '@interactjs/core/Interaction' import type { ListenerMap, Scope, SignalArgs, Plugin } from '@interactjs/core/scope' /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import './base' import basePlugin from './base' /* eslint-enable import/no-duplicates */ import { type PointerEvent } from './PointerEvent' declare module '@interactjs/core/Interaction' { interface Interaction { holdIntervalHandle?: any } } declare module '@interactjs/pointer-events/PointerEvent' { interface PointerEvent { count?: number } } declare module '@interactjs/pointer-events/base' { interface PointerEventOptions { holdRepeatInterval?: number } } function install(scope: Scope) { scope.usePlugin(basePlugin) const { pointerEvents } = scope // don't repeat by default pointerEvents.defaults.holdRepeatInterval = 0 pointerEvents.types.holdrepeat = scope.actions.phaselessTypes.holdrepeat = true } function onNew({ pointerEvent }: { pointerEvent: PointerEvent }) { if (pointerEvent.type !== 'hold') return pointerEvent.count = (pointerEvent.count || 0) + 1 } function onFired( { interaction, pointerEvent, eventTarget, targets }: SignalArgs['pointerEvents:fired'], scope: Scope, ) { if (pointerEvent.type !== 'hold' || !targets.length) return // get the repeat interval from the first eventable const interval = targets[0].eventable.options.holdRepeatInterval // don't repeat if the interval is 0 or less if (interval <= 0) return // set a timeout to fire the holdrepeat event interaction.holdIntervalHandle = setTimeout(() => { scope.pointerEvents.fire( { interaction, eventTarget, type: 'hold', pointer: pointerEvent, event: pointerEvent, }, scope, ) }, interval) } function endHoldRepeat({ interaction }: { interaction: Interaction }) { // set the interaction's holdStopTime property // to stop further holdRepeat events if (interaction.holdIntervalHandle) { clearInterval(interaction.holdIntervalHandle) interaction.holdIntervalHandle = null } } const holdRepeat: Plugin = { id: 'pointer-events/holdRepeat', install, listeners: ['move', 'up', 'cancel', 'endall'].reduce( (acc, enderTypes) => { ;(acc as any)[`pointerEvents:${enderTypes}`] = endHoldRepeat return acc }, { 'pointerEvents:new': onNew, 'pointerEvents:fired': onFired, } as ListenerMap, ), } export default holdRepeat ================================================ FILE: packages/@interactjs/pointer-events/interactableTargets.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { Scope, Plugin } from '@interactjs/core/scope' import type { Element } from '@interactjs/core/types' import extend from '@interactjs/utils/extend' import type { PointerEventOptions } from '@interactjs/pointer-events/base' declare module '@interactjs/core/Interactable' { interface Interactable { pointerEvents(options: Partial): this /** @internal */ __backCompatOption: (optionName: string, newValue: any) => any } } function install(scope: Scope) { const { Interactable } = scope Interactable.prototype.pointerEvents = function ( this: Interactable, options: Partial, ) { extend(this.events.options, options) return this } const __backCompatOption = Interactable.prototype._backCompatOption Interactable.prototype._backCompatOption = function (optionName, newValue) { const ret = __backCompatOption.call(this, optionName, newValue) if (ret === this) { this.events.options[optionName] = newValue } return ret } } const plugin: Plugin = { id: 'pointer-events/interactableTargets', install, listeners: { 'pointerEvents:collect-targets': ({ targets, node, type, eventTarget }, scope) => { scope.interactables.forEachMatch(node, (interactable: Interactable) => { const eventable = interactable.events const options = eventable.options if ( eventable.types[type] && eventable.types[type].length && interactable.testIgnoreAllow(options, node, eventTarget) ) { targets.push({ node, eventable, props: { interactable }, }) } }) }, 'interactable:new': ({ interactable }) => { interactable.events.getRect = function (element: Element) { return interactable.getRect(element) } }, 'interactable:set': ({ interactable, options }, scope) => { extend(interactable.events.options, scope.pointerEvents.defaults) extend(interactable.events.options, options.pointerEvents || {}) }, }, } export default plugin ================================================ FILE: packages/@interactjs/pointer-events/package.json ================================================ { "name": "@interactjs/pointer-events", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/pointer-events" }, "peerDependencies": { "@interactjs/core": "1.10.27", "@interactjs/utils": "1.10.27" }, "optionalDependencies": { "@interactjs/interact": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/pointer-events/plugin.ts ================================================ import type { Plugin } from '@interactjs/core/scope' /* eslint-disable import/no-duplicates -- for typescript module augmentations */ import './base' import './holdRepeat' import './interactableTargets' import * as pointerEvents from './base' import holdRepeat from './holdRepeat' import interactableTargets from './interactableTargets' /* eslint-enable import/no-duplicates */ const plugin: Plugin = { id: 'pointer-events', install(scope) { scope.usePlugin(pointerEvents) scope.usePlugin(holdRepeat) scope.usePlugin(interactableTargets) }, } export default plugin ================================================ FILE: packages/@interactjs/reflow/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/reflow/package.json ================================================ { "name": "@interactjs/reflow", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/reflow" }, "peerDependencies": { "@interactjs/core": "1.10.27", "@interactjs/utils": "1.10.27" }, "optionalDependencies": { "@interactjs/interact": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/reflow/plugin.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { DoAnyPhaseArg, Interaction } from '@interactjs/core/Interaction' import type { Scope, Plugin } from '@interactjs/core/scope' import type { ActionName, ActionProps, Element } from '@interactjs/core/types' import * as arr from '@interactjs/utils/arr' import { copyAction } from '@interactjs/utils/misc' import * as pointerUtils from '@interactjs/utils/pointerUtils' import { tlbrToXywh } from '@interactjs/utils/rect' declare module '@interactjs/core/scope' { interface SignalArgs { 'interactions:before-action-reflow': Omit 'interactions:action-reflow': DoAnyPhaseArg 'interactions:after-action-reflow': DoAnyPhaseArg } } declare module '@interactjs/core/Interactable' { interface Interactable { /** * ```js * const interactable = interact(target) * const drag = { name: drag, axis: 'x' } * const resize = { name: resize, edges: { left: true, bottom: true } * * interactable.reflow(drag) * interactable.reflow(resize) * ``` * * Start an action sequence to re-apply modifiers, check drops, etc. * * @param { Object } action The action to begin * @param { string } action.name The name of the action * @returns { Promise } A promise that resolves to the `Interactable` when actions on all targets have ended */ reflow(action: ActionProps): ReturnType } } declare module '@interactjs/core/Interaction' { interface Interaction { _reflowPromise: Promise _reflowResolve: (...args: unknown[]) => void } } declare module '@interactjs/core/InteractEvent' { interface PhaseMap { reflow?: true } } function install(scope: Scope) { const { Interactable } = scope scope.actions.phases.reflow = true Interactable.prototype.reflow = function (action: ActionProps) { return doReflow(this, action, scope) } } function doReflow( interactable: Interactable, action: ActionProps, scope: Scope, ): Promise { const elements = interactable.getAllElements() // tslint:disable-next-line variable-name const Promise = (scope.window as any).Promise const promises: Array> | null = Promise ? [] : null for (const element of elements) { const rect = interactable.getRect(element as HTMLElement | SVGElement) if (!rect) { break } const runningInteraction = arr.find(scope.interactions.list, (interaction: Interaction) => { return ( interaction.interacting() && interaction.interactable === interactable && interaction.element === element && interaction.prepared.name === action.name ) }) let reflowPromise: Promise if (runningInteraction) { runningInteraction.move() if (promises) { reflowPromise = runningInteraction._reflowPromise || new Promise((resolve: any) => { runningInteraction._reflowResolve = resolve }) } } else { const xywh = tlbrToXywh(rect) const coords = { page: { x: xywh.x, y: xywh.y }, client: { x: xywh.x, y: xywh.y }, timeStamp: scope.now(), } const event = pointerUtils.coordsToEvent(coords) reflowPromise = startReflow(scope, interactable, element, action, event) } if (promises) { promises.push(reflowPromise) } } return promises && Promise.all(promises).then(() => interactable) } function startReflow( scope: Scope, interactable: Interactable, element: Element, action: ActionProps, event: any, ) { const interaction = scope.interactions.new({ pointerType: 'reflow' }) const signalArg = { interaction, event, pointer: event, eventTarget: element, phase: 'reflow', } as const interaction.interactable = interactable interaction.element = element interaction.prevEvent = event interaction.updatePointer(event, event, element, true) pointerUtils.setZeroCoords(interaction.coords.delta) copyAction(interaction.prepared, action) interaction._doPhase(signalArg) const { Promise } = scope.window as unknown as { Promise: PromiseConstructor } const reflowPromise = Promise ? new Promise((resolve) => { interaction._reflowResolve = resolve }) : undefined interaction._reflowPromise = reflowPromise interaction.start(action, interactable, element) if (interaction._interacting) { interaction.move(signalArg) interaction.end(event) } else { interaction.stop() interaction._reflowResolve() } interaction.removePointer(event, event) return reflowPromise } const reflow: Plugin = { id: 'reflow', install, listeners: { // remove completed reflow interactions 'interactions:stop': ({ interaction }, scope) => { if (interaction.pointerType === 'reflow') { if (interaction._reflowResolve) { interaction._reflowResolve() } arr.remove(scope.interactions.list, interaction) } }, }, } export default reflow ================================================ FILE: packages/@interactjs/reflow/reflow.spec.ts ================================================ import type { Interactable } from '@interactjs/core/Interactable' import type { InteractEvent } from '@interactjs/core/InteractEvent' import * as helpers from '@interactjs/core/tests/_helpers' import type { ActionName, Point } from '@interactjs/core/types' import PromisePolyfill from 'promise-polyfill' import reflow from './plugin' const testAction = { name: 'TEST' as ActionName } const Promise_ = Promise describe('reflow', () => { test('sync', () => { const rect = Object.freeze({ top: 100, left: 200, bottom: 300, right: 400 }) const { scope, interactable } = helpers.testEnv({ plugins: [reflow], rect }) Object.assign(scope.actions, { TEST: {}, names: ['TEST'] }) // reflow method is added to Interactable.prototype expect(scope.Interactable.prototype.reflow instanceof Function).toBe(true) const fired: InteractEvent[] = [] let beforeReflowDelta: Point interactable.fire = ((iEvent: any) => { fired.push(iEvent) }) as any ;(interactable.options as any).TEST = { enabled: true } interactable.rectChecker(() => ({ ...rect })) // modify move coords scope.addListeners({ 'interactions:before-action-move': ({ interaction }) => { interaction.coords.cur.page = { x: rect.left + 100, y: rect.top - 50, } }, 'interactions:before-action-reflow': ({ interaction }) => { beforeReflowDelta = { ...interaction.coords.delta.page } }, }) interactable.reflow(testAction) const phases = ['reflow', 'start', 'move', 'end'] expect(phases.map((_phase, index) => fired[index]?.type)).toEqual(phases.map((phase) => `TEST${phase}`)) for (const index in phases) { const phase = phases[index] // `event #${index} is ${phase}` expect(fired[index].type).toBe(`TEST${phase}`) } const interaction = fired[0]._interaction // uses element top left for event coords expect(interaction.coords.start.page).toEqual({ x: rect.left, y: rect.top, }) const reflowMove = fired[2] // interaction delta is zero before-action-reflow expect(beforeReflowDelta!).toEqual({ x: 0, y: 0 }) // move delta is correct with modified interaction coords expect(reflowMove.delta).toEqual({ x: 100, y: -50 }) // reflow pointer was lifted expect(interaction.pointerIsDown).toBe(false) // reflow pointer was removed from interaction expect(interaction.pointers).toHaveLength(0) // interaction is removed from list expect(scope.interactions.list).not.toContain(interaction) }) test('async', async () => { const { scope } = helpers.testEnv({ plugins: [reflow] }) Object.assign(scope.actions, { TEST: {}, names: ['TEST'] }) let reflowEvent: any let promise: Promise const interactable = scope.interactables.new(scope.document.documentElement) const rect = Object.freeze({ top: 100, left: 200, bottom: 300, right: 400 }) interactable.rectChecker(() => ({ ...rect })) interactable.fire = ((iEvent: any) => { reflowEvent = iEvent }) as any ;(interactable.options as any).TEST = { enabled: true } // test with Promise implementation ;(scope.window as any).Promise = PromisePolyfill promise = interactable.reflow(testAction) // method returns a Promise if available expect(promise instanceof (scope.window as any).Promise).toBe(true) // reflow may end synchronously expect(reflowEvent.interaction.interacting()).toBe(false) // returned Promise resolves to interactable expect(await promise).toBe(interactable) let stoppedFromTimeout: boolean // block the end of the reflow interaction and stop it after a timeout scope.addListeners({ 'interactions:before-action-end': ({ interaction }) => { setTimeout(() => { interaction.stop() stoppedFromTimeout = true }, 0) return false }, }) stoppedFromTimeout = false promise = interactable.reflow(testAction) // interaction continues if end is blocked expect(reflowEvent.interaction.interacting() && !stoppedFromTimeout).toBe(true) await promise // interaction is stopped after promise is resolved expect(reflowEvent.interaction.interacting() && stoppedFromTimeout).toBe(false) // test without Promise implementation stoppedFromTimeout = false ;(scope.window as any).Promise = undefined promise = interactable.reflow(testAction) // method returns null if no Proise is avilable expect(promise).toBeNull() // interaction continues if end is blocked without Promise expect(reflowEvent.interaction.interacting() && !stoppedFromTimeout).toBe(true) await new Promise_((resolve) => setTimeout(() => { // interaction is stopped after timeout without Promised expect(reflowEvent.interaction.interacting() || !stoppedFromTimeout).toBe(false) resolve() }, 0), ) }) }) ================================================ FILE: packages/@interactjs/snappers/all.ts ================================================ /* eslint-disable import/no-named-as-default, import/no-unresolved */ export { default as edgeTarget } from './edgeTarget' export { default as elements } from './elements' export { default as grid } from './grid' ================================================ FILE: packages/@interactjs/snappers/edgeTarget.stub.ts ================================================ export default () => {} ================================================ FILE: packages/@interactjs/snappers/edgeTarget.ts ================================================ export default () => {} ================================================ FILE: packages/@interactjs/snappers/elements.stub.ts ================================================ export default () => {} ================================================ FILE: packages/@interactjs/snappers/elements.ts ================================================ export default () => {} ================================================ FILE: packages/@interactjs/snappers/grid.ts ================================================ import type { Rect, Point } from '@interactjs/core/types' import type { SnapFunction, SnapTarget } from '@interactjs/modifiers/snap/pointer' export interface GridOptionsBase { range?: number limits?: Rect offset?: Point } export interface GridOptionsXY extends GridOptionsBase { x: number y: number } export interface GridOptionsTopLeft extends GridOptionsBase { top?: number left?: number } export interface GridOptionsBottomRight extends GridOptionsBase { bottom?: number right?: number } export interface GridOptionsWidthHeight extends GridOptionsBase { width?: number height?: number } export type GridOptions = GridOptionsXY | GridOptionsTopLeft | GridOptionsBottomRight | GridOptionsWidthHeight export default (grid: GridOptions) => { const coordFields = ( [ ['x', 'y'], ['left', 'top'], ['right', 'bottom'], ['width', 'height'], ] as const ).filter(([xField, yField]) => xField in grid || yField in grid) const gridFunc: SnapFunction & { grid: typeof grid coordFields: typeof coordFields } = (x, y) => { const { range, limits = { left: -Infinity, right: Infinity, top: -Infinity, bottom: Infinity, }, offset = { x: 0, y: 0 }, } = grid const result: SnapTarget & { grid: typeof grid } = { range, grid, x: null as number, y: null as number } for (const [xField, yField] of coordFields) { const gridx = Math.round((x - offset.x) / (grid as any)[xField]) const gridy = Math.round((y - offset.y) / (grid as any)[yField]) result[xField] = Math.max(limits.left, Math.min(limits.right, gridx * (grid as any)[xField] + offset.x)) result[yField] = Math.max(limits.top, Math.min(limits.bottom, gridy * (grid as any)[yField] + offset.y)) } return result } gridFunc.grid = grid gridFunc.coordFields = coordFields return gridFunc } ================================================ FILE: packages/@interactjs/snappers/package.json ================================================ { "name": "@interactjs/snappers", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/snappers" }, "peerDependencies": { "@interactjs/utils": "1.10.27" }, "optionalDependencies": { "@interactjs/interact": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/snappers/plugin.ts ================================================ import type { Plugin } from '@interactjs/core/scope' import extend from '@interactjs/utils/extend' import * as allSnappers from './all' declare module '@interactjs/core/InteractStatic' { export interface InteractStatic { snappers: typeof allSnappers createSnapGrid: typeof allSnappers.grid } } const snappersPlugin: Plugin = { id: 'snappers', install(scope) { const { interactStatic: interact } = scope interact.snappers = extend(interact.snappers || {}, allSnappers) interact.createSnapGrid = interact.snappers.grid }, } export default snappersPlugin ================================================ FILE: packages/@interactjs/types/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/types/index.ts ================================================ /* eslint-disable import/no-extraneous-dependencies */ import type { InteractEvent as _InteractEvent, EventPhase } from '@interactjs/core/InteractEvent' import type * as interaction from '@interactjs/core/Interaction' import type { ActionName, ActionProps as _ActionProps } from '@interactjs/core/types' // import module augmentations import '@interactjs/interactjs' export * from '@interactjs/core/types' export type { Plugin } from '@interactjs/core/scope' export type { EventPhase } from '@interactjs/core/InteractEvent' export type { Options } from '@interactjs/core/options' export type { PointerEvent } from '@interactjs/pointer-events/PointerEvent' export type { Interactable } from '@interactjs/core/Interactable' export type { DragEvent } from '@interactjs/actions/drag/plugin' export type { DropEvent } from '@interactjs/actions/drop/DropEvent' export type { GestureEvent } from '@interactjs/actions/gesture/plugin' export type { ResizeEvent } from '@interactjs/actions/resize/plugin' export type { SnapFunction, SnapTarget } from '@interactjs/modifiers/snap/pointer' export type ActionProps = _ActionProps export type Interaction = interaction.Interaction export type InteractionProxy = interaction.InteractionProxy export type PointerArgProps = interaction.PointerArgProps export type InteractEvent = _InteractEvent< T, P > ================================================ FILE: packages/@interactjs/types/package.json ================================================ { "name": "@interactjs/types", "version": "1.10.27", "main": "index", "module": "index", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/types" }, "typings": "typings.d.ts", "devDependencies": { "@interactjs/actions": "1.10.27", "@interactjs/arrange": "1.10.27", "@interactjs/auto-scroll": "1.10.27", "@interactjs/auto-start": "1.10.27", "@interactjs/core": "1.10.27", "@interactjs/dev-tools": "1.10.27", "@interactjs/inertia": "1.10.27", "@interactjs/interact": "1.10.27", "@interactjs/interactjs": "1.10.27", "@interactjs/modifiers": "1.10.27", "@interactjs/pointer-events": "1.10.27", "@interactjs/reflow": "1.10.27", "@interactjs/snappers": "1.10.27", "@interactjs/utils": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: packages/@interactjs/types/types.spec.ts ================================================ /** @jest-environment node */ import path from 'path' import * as execTypes from '@interactjs/_dev/scripts/execTypes' import { mkdirp } from 'mkdirp' import * as shell from 'shelljs' import temp from 'temp' jest.setTimeout(15000) test('typings', async () => { shell.config.fatal = true const tempDir = temp.track().mkdirSync('testProject') const modulesDir = path.join(tempDir, 'node_modules') const tempTypesDir = path.join(modulesDir, '@interactjs', 'types') const interactDir = path.join(modulesDir, 'interactjs') await mkdirp(interactDir) // run .d.ts generation script with output to temp dir node_modules await execTypes.combined(tempTypesDir) // copy .d.ts and package.json files of deps to temp dir shell.cp(path.join('packages', 'interactjs', '{*.d.ts,package.json}'), interactDir) shell.cp(path.join('packages', '@interactjs', 'types', '{*.d.ts,package.json}'), tempTypesDir) shell.cp('-R', path.join(process.cwd(), 'test', 'fixtures', 'dependentTsProject', '*'), tempDir) expect(() => { shell.exec(`${getBin('tsc')} -b`, { cwd: tempDir }) }).not.toThrow() shell.config.reset() }) const nodeBins = path.join(process.cwd(), 'node_modules', '.bin') const getBin = (name: string) => path.join(nodeBins, name) ================================================ FILE: packages/@interactjs/utils/ElementState.stub.ts ================================================ export default {} ================================================ FILE: packages/@interactjs/utils/ElementState.ts ================================================ export default {} ================================================ FILE: packages/@interactjs/utils/README.md ================================================

This package is an internal part of interactjs and is not meant to be used independently as each update may introduce breaking changes

================================================ FILE: packages/@interactjs/utils/arr.ts ================================================ type Filter = (element: T, index: number, array: T[]) => boolean export const contains = (array: T[], target: T) => array.indexOf(target) !== -1 export const remove = (array: T[], target: T) => array.splice(array.indexOf(target), 1) export const merge = (target: Array, source: U[]) => { for (const item of source) { target.push(item) } return target } export const from = (source: ArrayLike) => merge([] as T[], source as T[]) export const findIndex = (array: T[], func: Filter) => { for (let i = 0; i < array.length; i++) { if (func(array[i], i, array)) { return i } } return -1 } export const find = (array: T[], func: Filter): T | undefined => array[findIndex(array, func)] ================================================ FILE: packages/@interactjs/utils/browser.ts ================================================ import domObjects from './domObjects' import is from './is' const browser = { init, supportsTouch: null as boolean, supportsPointerEvent: null as boolean, isIOS7: null as boolean, isIOS: null as boolean, isIe9: null as boolean, isOperaMobile: null as boolean, prefixedMatchesSelector: null as 'matches', pEventTypes: null as { up: string down: string over: string out: string move: string cancel: string }, wheelEvent: null as string, } function init(window: any) { const Element = domObjects.Element const navigator: Partial = window.navigator || {} // Does the browser support touch input? browser.supportsTouch = 'ontouchstart' in window || (is.func(window.DocumentTouch) && domObjects.document instanceof window.DocumentTouch) // Does the browser support PointerEvents // https://github.com/taye/interact.js/issues/703#issuecomment-471570492 browser.supportsPointerEvent = (navigator as any).pointerEnabled !== false && !!domObjects.PointerEvent browser.isIOS = /iP(hone|od|ad)/.test(navigator.platform) // scrolling doesn't change the result of getClientRects on iOS 7 browser.isIOS7 = /iP(hone|od|ad)/.test(navigator.platform) && /OS 7[^\d]/.test(navigator.appVersion) browser.isIe9 = /MSIE 9/.test(navigator.userAgent) // Opera Mobile must be handled differently browser.isOperaMobile = navigator.appName === 'Opera' && browser.supportsTouch && /Presto/.test(navigator.userAgent) // prefix matchesSelector browser.prefixedMatchesSelector = ( 'matches' in Element.prototype ? 'matches' : 'webkitMatchesSelector' in Element.prototype ? 'webkitMatchesSelector' : 'mozMatchesSelector' in Element.prototype ? 'mozMatchesSelector' : 'oMatchesSelector' in Element.prototype ? 'oMatchesSelector' : 'msMatchesSelector' ) as 'matches' browser.pEventTypes = browser.supportsPointerEvent ? domObjects.PointerEvent === window.MSPointerEvent ? { up: 'MSPointerUp', down: 'MSPointerDown', over: 'mouseover', out: 'mouseout', move: 'MSPointerMove', cancel: 'MSPointerCancel', } : { up: 'pointerup', down: 'pointerdown', over: 'pointerover', out: 'pointerout', move: 'pointermove', cancel: 'pointercancel', } : null // because Webkit and Opera still use 'mousewheel' event type browser.wheelEvent = domObjects.document && 'onmousewheel' in domObjects.document ? 'mousewheel' : 'wheel' } export default browser ================================================ FILE: packages/@interactjs/utils/center.ts ================================================ import type { Rect } from '@interactjs/core/types' export default (rect: Rect) => ({ x: rect.left + (rect.right - rect.left) / 2, y: rect.top + (rect.bottom - rect.top) / 2, }) ================================================ FILE: packages/@interactjs/utils/clone.ts ================================================ import * as arr from './arr' import is from './is' // tslint:disable-next-line ban-types export default function clone(source: T): Partial { const dest = {} as Partial for (const prop in source) { const value = source[prop] if (is.plainObject(value)) { dest[prop] = clone(value) as any } else if (is.array(value)) { dest[prop] = arr.from(value) as typeof value } else { dest[prop] = value } } return dest } ================================================ FILE: packages/@interactjs/utils/displace.stub.ts ================================================ export default {} ================================================ FILE: packages/@interactjs/utils/displace.ts ================================================ export default {} ================================================ FILE: packages/@interactjs/utils/domObjects.ts ================================================ const domObjects: { init: any document: Document DocumentFragment: typeof DocumentFragment SVGElement: typeof SVGElement SVGSVGElement: typeof SVGSVGElement SVGElementInstance: any Element: typeof Element HTMLElement: typeof HTMLElement Event: typeof Event Touch: typeof Touch PointerEvent: typeof PointerEvent } = { init, document: null, DocumentFragment: null, SVGElement: null, SVGSVGElement: null, SVGElementInstance: null, Element: null, HTMLElement: null, Event: null, Touch: null, PointerEvent: null, } function blank() {} export default domObjects function init(window: Window) { const win = window as any domObjects.document = win.document domObjects.DocumentFragment = win.DocumentFragment || blank domObjects.SVGElement = win.SVGElement || blank domObjects.SVGSVGElement = win.SVGSVGElement || blank domObjects.SVGElementInstance = win.SVGElementInstance || blank domObjects.Element = win.Element || blank domObjects.HTMLElement = win.HTMLElement || domObjects.Element domObjects.Event = win.Event domObjects.Touch = win.Touch || blank domObjects.PointerEvent = win.PointerEvent || win.MSPointerEvent } ================================================ FILE: packages/@interactjs/utils/domUtils.spec.ts ================================================ import domObjects from './domObjects' import { indexOfDeepestElement } from './domUtils' interface MockNode { name: string lastChild: any parentNode: MockNode | null ownerDocument: MockNode | null host?: MockNode } test('utils/domUtils/indexOfDeepestElement', () => { document.body.innerHTML = `
` domObjects.init(document) const ownerDocument: MockNode = { name: 'Owner Document', lastChild: null as any, parentNode: null, ownerDocument: null, } const html: MockNode = { name: 'html', lastChild: null as any, ownerDocument, parentNode: ownerDocument, } const body: MockNode = { name: 'body', lastChild: null, ownerDocument, parentNode: html } const wrapper: MockNode = { name: 'wrapper', ownerDocument, parentNode: body, lastChild: null } const a: MockNode = { name: 'a', ownerDocument, parentNode: wrapper, lastChild: null } const b1: MockNode = { name: 'b1', ownerDocument, parentNode: a, lastChild: null } const b2: MockNode = { name: 'b2', ownerDocument, parentNode: a, lastChild: null } const c1: MockNode = { name: 'c1', ownerDocument, parentNode: b1, lastChild: null } const c2: MockNode = { name: 'c2', ownerDocument, parentNode: b1, lastChild: null } const d1: MockNode = { name: 'd1', ownerDocument, parentNode: c1, lastChild: null } const d1Comp: MockNode = { name: 'd1_comp', ownerDocument, parentNode: d1, lastChild: null } const d2Shadow: MockNode = { name: 'd2_shadow', ownerDocument, parentNode: null, lastChild: null, host: d1Comp, } ownerDocument.lastChild = html html.lastChild = body body.lastChild = wrapper a.lastChild = b2 b1.lastChild = c2 b2.lastChild = null c1.lastChild = d1 c2.lastChild = null d1.lastChild = d1 wrapper.lastChild = a const deepestShadow = [null, d2Shadow, c1, b1, a] as unknown as HTMLElement[] expect(indexOfDeepestElement(deepestShadow)).toBe(deepestShadow.indexOf(d2Shadow as any)) const noShadow = [null, d1, c1, b1] as unknown as HTMLElement[] // only chooses elements that are passed in expect(indexOfDeepestElement(noShadow)).toBe(noShadow.indexOf(d1 as any)) const siblings: NodeListOf = document.querySelectorAll('#topDiv > *') // last sibling is deepest with equal zIndex expect(indexOfDeepestElement(siblings)).toBe(2) siblings[0].style.zIndex = '2' siblings[1].style.zIndex = '2' siblings[2].style.zIndex = '1' // works with shadow root // sibling with higher z-index is selected expect(indexOfDeepestElement(siblings)).toBe(1) const nodeWithoutParent: MockNode = { name: 'd1', ownerDocument, parentNode: null, lastChild: null } const brokenElementCollection = [nodeWithoutParent, d1, c2] as unknown as HTMLElement[] expect(indexOfDeepestElement(brokenElementCollection)).toBe(0) }) ================================================ FILE: packages/@interactjs/utils/domUtils.ts ================================================ import type { Rect, Target, Element } from '@interactjs/core/types' import browser from './browser' import domObjects from './domObjects' import is from './is' import * as win from './window' export function nodeContains(parent: Node, child: Node) { if (parent.contains) { return parent.contains(child as Node) } while (child) { if (child === parent) { return true } child = (child as Node).parentNode } return false } export function closest(element: Node, selector: string) { while (is.element(element)) { if (matchesSelector(element, selector)) { return element } element = parentNode(element) } return null } export function parentNode(node: Node | Document) { let parent = node.parentNode if (is.docFrag(parent)) { // skip past #shado-root fragments // tslint:disable-next-line while ((parent = (parent as any).host) && is.docFrag(parent)) { continue } return parent } return parent } export function matchesSelector(element: Element, selector: string) { // remove /deep/ from selectors if shadowDOM polyfill is used if (win.window !== win.realWindow) { selector = selector.replace(/\/deep\//g, ' ') } return element[browser.prefixedMatchesSelector](selector) } const getParent = (el: Node | Document | ShadowRoot) => el.parentNode || (el as ShadowRoot).host // Test for the element that's "above" all other qualifiers export function indexOfDeepestElement(elements: Element[] | NodeListOf) { let deepestNodeParents: Node[] = [] let deepestNodeIndex: number for (let i = 0; i < elements.length; i++) { const currentNode = elements[i] const deepestNode: Node = elements[deepestNodeIndex] // node may appear in elements array multiple times if (!currentNode || i === deepestNodeIndex) { continue } if (!deepestNode) { deepestNodeIndex = i continue } const currentNodeParent = getParent(currentNode) const deepestNodeParent = getParent(deepestNode) // check if the deepest or current are document.documentElement/rootElement // - if the current node is, do nothing and continue if (currentNodeParent === currentNode.ownerDocument) { continue } // - if deepest is, update with the current node and continue to next else if (deepestNodeParent === currentNode.ownerDocument) { deepestNodeIndex = i continue } // compare zIndex of siblings if (currentNodeParent === deepestNodeParent) { if (zIndexIsHigherThan(currentNode, deepestNode)) { deepestNodeIndex = i } continue } // populate the ancestry array for the latest deepest node deepestNodeParents = deepestNodeParents.length ? deepestNodeParents : getNodeParents(deepestNode) let ancestryStart: Node // if the deepest node is an HTMLElement and the current node is a non root svg element if ( deepestNode instanceof domObjects.HTMLElement && currentNode instanceof domObjects.SVGElement && !(currentNode instanceof domObjects.SVGSVGElement) ) { // TODO: is this check necessary? Was this for HTML elements embedded in SVG? if (currentNode === deepestNodeParent) { continue } ancestryStart = currentNode.ownerSVGElement } else { ancestryStart = currentNode } const currentNodeParents = getNodeParents(ancestryStart, deepestNode.ownerDocument) let commonIndex = 0 // get (position of closest common ancestor) + 1 while ( currentNodeParents[commonIndex] && currentNodeParents[commonIndex] === deepestNodeParents[commonIndex] ) { commonIndex++ } const parents = [ currentNodeParents[commonIndex - 1], currentNodeParents[commonIndex], deepestNodeParents[commonIndex], ] if (parents[0]) { let child = parents[0].lastChild while (child) { if (child === parents[1]) { deepestNodeIndex = i deepestNodeParents = currentNodeParents break } else if (child === parents[2]) { break } child = child.previousSibling } } } return deepestNodeIndex } function getNodeParents(node: Node, limit?: Node) { const parents: Node[] = [] let parent: Node = node let parentParent: Node while ((parentParent = getParent(parent)) && parent !== limit && parentParent !== parent.ownerDocument) { parents.unshift(parent) parent = parentParent } return parents } function zIndexIsHigherThan(higherNode: Node, lowerNode: Node) { const higherIndex = parseInt(win.getWindow(higherNode).getComputedStyle(higherNode).zIndex, 10) || 0 const lowerIndex = parseInt(win.getWindow(lowerNode).getComputedStyle(lowerNode).zIndex, 10) || 0 return higherIndex >= lowerIndex } export function matchesUpTo(element: Element, selector: string, limit: Node) { while (is.element(element)) { if (matchesSelector(element, selector)) { return true } element = parentNode(element) as Element if (element === limit) { return matchesSelector(element, selector) } } return false } export function getActualElement(element: Element) { return (element as any).correspondingUseElement || element } export function getScrollXY(relevantWindow?: Window) { relevantWindow = relevantWindow || win.window return { x: relevantWindow.scrollX || relevantWindow.document.documentElement.scrollLeft, y: relevantWindow.scrollY || relevantWindow.document.documentElement.scrollTop, } } export function getElementClientRect(element: Element): Required { const clientRect = element instanceof domObjects.SVGElement ? element.getBoundingClientRect() : element.getClientRects()[0] return ( clientRect && { left: clientRect.left, right: clientRect.right, top: clientRect.top, bottom: clientRect.bottom, width: clientRect.width || clientRect.right - clientRect.left, height: clientRect.height || clientRect.bottom - clientRect.top, } ) } export function getElementRect(element: Element) { const clientRect = getElementClientRect(element) if (!browser.isIOS7 && clientRect) { const scroll = getScrollXY(win.getWindow(element)) clientRect.left += scroll.x clientRect.right += scroll.x clientRect.top += scroll.y clientRect.bottom += scroll.y } return clientRect } export function getPath(node: Node | Document) { const path = [] while (node) { path.push(node) node = parentNode(node) } return path } export function trySelector(value: Target) { if (!is.string(value)) { return false } // an exception will be raised if it is invalid domObjects.document.querySelector(value) return true } ================================================ FILE: packages/@interactjs/utils/exchange.stub.ts ================================================ export default {} ================================================ FILE: packages/@interactjs/utils/exchange.ts ================================================ export default {} ================================================ FILE: packages/@interactjs/utils/extend.ts ================================================ export default function extend(dest: U & Partial, source: T): T & U { for (const prop in source) { ;(dest as unknown as T)[prop] = source[prop] } const ret = dest as T & U return ret } ================================================ FILE: packages/@interactjs/utils/getOriginXY.ts ================================================ import type { PerActionDefaults } from '@interactjs/core/options' import type { ActionName, HasGetRect } from '@interactjs/core/types' import { rectToXY, resolveRectLike } from './rect' export default function getOriginXY( target: HasGetRect & { options: PerActionDefaults }, element: Node, actionName?: ActionName, ) { const actionOptions = actionName && (target.options as any)[actionName] const actionOrigin = actionOptions && actionOptions.origin const origin = actionOrigin || target.options.origin const originRect = resolveRectLike(origin, target, element, [target && element]) return rectToXY(originRect) || { x: 0, y: 0 } } ================================================ FILE: packages/@interactjs/utils/hypot.ts ================================================ export default (x: number, y: number) => Math.sqrt(x * x + y * y) ================================================ FILE: packages/@interactjs/utils/is.ts ================================================ import isWindow from './isWindow' import * as win from './window' const window = (thing: any): thing is Window => thing === win.window || isWindow(thing) const docFrag = (thing: any): thing is DocumentFragment => object(thing) && thing.nodeType === 11 const object = (thing: any): thing is { [index: string]: any } => !!thing && typeof thing === 'object' const func = (thing: any): thing is (...args: any[]) => any => typeof thing === 'function' const number = (thing: any): thing is number => typeof thing === 'number' const bool = (thing: any): thing is boolean => typeof thing === 'boolean' const string = (thing: any): thing is string => typeof thing === 'string' const element = (thing: any): thing is HTMLElement | SVGElement => { if (!thing || typeof thing !== 'object') { return false } const _window = win.getWindow(thing) || win.window return /object|function/.test(typeof Element) ? thing instanceof Element || thing instanceof _window.Element : thing.nodeType === 1 && typeof thing.nodeName === 'string' } const plainObject: typeof object = (thing: any): thing is { [index: string]: any } => object(thing) && !!thing.constructor && /function Object\b/.test(thing.constructor.toString()) const array = (thing: any): thing is T[] => object(thing) && typeof thing.length !== 'undefined' && func(thing.splice) export default { window, docFrag, object, func, number, bool, string, element, plainObject, array, } ================================================ FILE: packages/@interactjs/utils/isNonNativeEvent.ts ================================================ import type { Actions } from '@interactjs/core/types' export default function isNonNativeEvent(type: string, actions: Actions) { if (actions.phaselessTypes[type]) { return true } for (const name in actions.map) { if (type.indexOf(name) === 0 && type.substr(name.length) in actions.phases) { return true } } return false } ================================================ FILE: packages/@interactjs/utils/isWindow.ts ================================================ export default (thing: any) => !!(thing && thing.Window) && thing instanceof thing.Window ================================================ FILE: packages/@interactjs/utils/misc.ts ================================================ import type { ActionName, ActionProps } from '@interactjs/core/types' import { window } from './window' export function warnOnce(this: T, method: (...args: any[]) => any, message: string) { let warned = false return function (this: T) { if (!warned) { ;(window as any).console.warn(message) warned = true } return method.apply(this, arguments) } } export function copyAction(dest: ActionProps, src: ActionProps) { dest.name = src.name dest.axis = src.axis dest.edges = src.edges return dest } export const sign = (n: number) => (n >= 0 ? 1 : -1) ================================================ FILE: packages/@interactjs/utils/normalizeListeners.spec.ts ================================================ import normalizeListeners from './normalizeListeners' test('utils/normalizeListeners', () => { const a = () => {} const b = () => {} const c = () => {} // single type, single listener function expect(normalizeListeners('type1', a)).toEqual({ type1: [a], }) // multiple types, single listener function expect(normalizeListeners('type1 type2', a)).toEqual({ type1: [a], type2: [a], }) // array of types equivalent to space separated string expect(normalizeListeners('type1 type2', a)).toEqual(normalizeListeners(['type1', 'type2'], a)) // single type, multiple listener functions expect(normalizeListeners('type1', [a, b])).toEqual({ type1: [a, b], }) // single type prefix, object of { suffix: [fn, ...] } expect(normalizeListeners('prefix', { _1: [a, b], _2: [b, c] })).toEqual({ prefix_1: [a, b], prefix_2: [b, c], }) // multiple type prefixes, single length array of { suffix: [fn, ...] } expect(normalizeListeners('prefix1 prefix2', [{ _1: [a, b], _2: [b, c] }])).toEqual({ prefix1_1: [a, b], prefix1_2: [b, c], prefix2_1: [a, b], prefix2_2: [b, c], }) // object of { suffix: [fn, ...] } as type arg expect(normalizeListeners({ _1: [a, b], _2: [b, c] })).toEqual({ _1: [a, b], _2: [b, c], }) // object of { "suffix1 suffix2": [fn, ...], ... } as type arg expect(normalizeListeners({ '_1 _2': [a, b], _3: [b, c] })).toEqual({ _1: [a, b], _2: [a, b], _3: [b, c], }) // single type prefix, object of { "suffix1 suffix2": [fn, ...], ... } expect(normalizeListeners('prefix', { '_1 _2': [a, b], _3: [b, c] })).toEqual({ prefix_1: [a, b], prefix_2: [a, b], prefix_3: [b, c], }) // filter expect(normalizeListeners('ignore', [{ _1: a, '': b }], (type) => !type.startsWith('ignore'))).toEqual({}) expect( normalizeListeners( { ignore: { _1: a }, ig: { nore: b }, allow: { _x: c } }, undefined, (type) => !type.startsWith('ignore'), ), ).toEqual({ allow_x: [c] }) }) ================================================ FILE: packages/@interactjs/utils/normalizeListeners.ts ================================================ import type { EventTypes, Listener, ListenersArg } from '@interactjs/core/types' import is from './is' export interface NormalizedListeners { [type: string]: Listener[] } export default function normalize( type: EventTypes, listeners?: ListenersArg | ListenersArg[] | null, filter = (_typeOrPrefix: string) => true, result?: NormalizedListeners, ): NormalizedListeners { result = result || {} if (is.string(type) && type.search(' ') !== -1) { type = split(type) } if (is.array(type)) { type.forEach((t) => normalize(t, listeners, filter, result)) return result } // before: type = [{ drag: () => {} }], listeners = undefined // after: type = '' , listeners = [{ drag: () => {} }] if (is.object(type)) { listeners = type type = '' } if (is.func(listeners) && filter(type)) { result[type] = result[type] || [] result[type].push(listeners) } else if (is.array(listeners)) { for (const l of listeners) { normalize(type, l, filter, result) } } else if (is.object(listeners)) { for (const prefix in listeners) { const combinedTypes = split(prefix).map((p) => `${type}${p}`) normalize(combinedTypes, listeners[prefix], filter, result) } } return result as NormalizedListeners } function split(type: string) { return type.trim().split(/ +/) } ================================================ FILE: packages/@interactjs/utils/package.json ================================================ { "name": "@interactjs/utils", "version": "1.10.27", "type": "module", "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git", "directory": "packages/@interactjs/utils" }, "peerDependencies": { "@interactjs/feedback": "1.10.27", "@interactjs/symbol-tree": "1.10.27" }, "publishConfig": { "access": "public" }, "sideEffects": false, "license": "MIT" } ================================================ FILE: packages/@interactjs/utils/pointerExtend.ts ================================================ const VENDOR_PREFIXES = ['webkit', 'moz'] export default function pointerExtend(dest: Partial }>, source: T) { dest.__set ||= {} as any for (const prop in source) { // skip deprecated prefixed properties if (VENDOR_PREFIXES.some((prefix) => prop.indexOf(prefix) === 0)) continue if (typeof dest[prop] !== 'function' && prop !== '__set') { Object.defineProperty(dest, prop, { get() { if (prop in dest.__set) return dest.__set[prop] return (dest.__set[prop] = source[prop] as any) }, set(value: any) { dest.__set[prop] = value }, configurable: true, }) } } return dest } ================================================ FILE: packages/@interactjs/utils/pointerUtils.ts ================================================ import type { InteractEvent } from '@interactjs/core/InteractEvent' import type { CoordsSetMember, PointerType, Point, PointerEventType, Element } from '@interactjs/core/types' import browser from './browser' import dom from './domObjects' import * as domUtils from './domUtils' import hypot from './hypot' import is from './is' import pointerExtend from './pointerExtend' export function copyCoords(dest: CoordsSetMember, src: CoordsSetMember) { dest.page = dest.page || ({} as any) dest.page.x = src.page.x dest.page.y = src.page.y dest.client = dest.client || ({} as any) dest.client.x = src.client.x dest.client.y = src.client.y dest.timeStamp = src.timeStamp } export function setCoordDeltas(targetObj: CoordsSetMember, prev: CoordsSetMember, cur: CoordsSetMember) { targetObj.page.x = cur.page.x - prev.page.x targetObj.page.y = cur.page.y - prev.page.y targetObj.client.x = cur.client.x - prev.client.x targetObj.client.y = cur.client.y - prev.client.y targetObj.timeStamp = cur.timeStamp - prev.timeStamp } export function setCoordVelocity(targetObj: CoordsSetMember, delta: CoordsSetMember) { const dt = Math.max(delta.timeStamp / 1000, 0.001) targetObj.page.x = delta.page.x / dt targetObj.page.y = delta.page.y / dt targetObj.client.x = delta.client.x / dt targetObj.client.y = delta.client.y / dt targetObj.timeStamp = dt } export function setZeroCoords(targetObj: CoordsSetMember) { targetObj.page.x = 0 targetObj.page.y = 0 targetObj.client.x = 0 targetObj.client.y = 0 } export function isNativePointer(pointer: any) { return pointer instanceof dom.Event || pointer instanceof dom.Touch } // Get specified X/Y coords for mouse or event.touches[0] export function getXY(type: string, pointer: PointerType | InteractEvent, xy: Point) { xy = xy || ({} as Point) type = type || 'page' xy.x = pointer[(type + 'X') as 'pageX'] xy.y = pointer[(type + 'Y') as 'pageY'] return xy } export function getPageXY(pointer: PointerType | InteractEvent, page?: Point) { page = page || { x: 0, y: 0 } // Opera Mobile handles the viewport and scrolling oddly if (browser.isOperaMobile && isNativePointer(pointer)) { getXY('screen', pointer, page) page.x += window.scrollX page.y += window.scrollY } else { getXY('page', pointer, page) } return page } export function getClientXY(pointer: PointerType, client: Point) { client = client || ({} as any) if (browser.isOperaMobile && isNativePointer(pointer)) { // Opera Mobile handles the viewport and scrolling oddly getXY('screen', pointer, client) } else { getXY('client', pointer, client) } return client } export function getPointerId(pointer: { pointerId?: number; identifier?: number; type?: string }) { return is.number(pointer.pointerId) ? pointer.pointerId! : pointer.identifier! } export function setCoords(dest: CoordsSetMember, pointers: any[], timeStamp: number) { const pointer = pointers.length > 1 ? pointerAverage(pointers) : pointers[0] getPageXY(pointer, dest.page) getClientXY(pointer, dest.client) dest.timeStamp = timeStamp } export function getTouchPair(event: TouchEvent | PointerType[]) { const touches: PointerType[] = [] // array of touches is supplied if (is.array(event)) { touches[0] = event[0] touches[1] = event[1] } // an event else { if (event.type === 'touchend') { if (event.touches.length === 1) { touches[0] = event.touches[0] touches[1] = event.changedTouches[0] } else if (event.touches.length === 0) { touches[0] = event.changedTouches[0] touches[1] = event.changedTouches[1] } } else { touches[0] = event.touches[0] touches[1] = event.touches[1] } } return touches } export function pointerAverage(pointers: PointerType[]) { const average = { pageX: 0, pageY: 0, clientX: 0, clientY: 0, screenX: 0, screenY: 0, } type CoordKeys = keyof typeof average for (const pointer of pointers) { for (const prop in average) { average[prop as CoordKeys] += pointer[prop as CoordKeys] } } for (const prop in average) { average[prop as CoordKeys] /= pointers.length } return average } export function touchBBox(event: PointerType[]) { if (!event.length) { return null } const touches = getTouchPair(event) const minX = Math.min(touches[0].pageX, touches[1].pageX) const minY = Math.min(touches[0].pageY, touches[1].pageY) const maxX = Math.max(touches[0].pageX, touches[1].pageX) const maxY = Math.max(touches[0].pageY, touches[1].pageY) return { x: minX, y: minY, left: minX, top: minY, right: maxX, bottom: maxY, width: maxX - minX, height: maxY - minY, } } export function touchDistance(event: PointerType[] | TouchEvent, deltaSource: string) { const sourceX = (deltaSource + 'X') as 'pageX' const sourceY = (deltaSource + 'Y') as 'pageY' const touches = getTouchPair(event) const dx = touches[0][sourceX] - touches[1][sourceX] const dy = touches[0][sourceY] - touches[1][sourceY] return hypot(dx, dy) } export function touchAngle(event: PointerType[] | TouchEvent, deltaSource: string) { const sourceX = (deltaSource + 'X') as 'pageX' const sourceY = (deltaSource + 'Y') as 'pageY' const touches = getTouchPair(event) const dx = touches[1][sourceX] - touches[0][sourceX] const dy = touches[1][sourceY] - touches[0][sourceY] const angle = (180 * Math.atan2(dy, dx)) / Math.PI return angle } export function getPointerType(pointer: { pointerType?: string; identifier?: number; type?: string }) { return is.string(pointer.pointerType) ? pointer.pointerType : is.number(pointer.pointerType) ? [undefined, undefined, 'touch', 'pen', 'mouse'][pointer.pointerType]! : // if the PointerEvent API isn't available, then the "pointer" must // be either a MouseEvent, TouchEvent, or Touch object /touch/.test(pointer.type || '') || pointer instanceof dom.Touch ? 'touch' : 'mouse' } // [ event.target, event.currentTarget ] export function getEventTargets(event: Event) { const path = is.func(event.composedPath) ? (event.composedPath() as Element[]) : (event as unknown as { path: Element[] }).path return [ domUtils.getActualElement(path ? path[0] : (event.target as Element)), domUtils.getActualElement(event.currentTarget as Element), ] } export function newCoords(): CoordsSetMember { return { page: { x: 0, y: 0 }, client: { x: 0, y: 0 }, timeStamp: 0, } } export function coordsToEvent(coords: MockCoords) { const event = { coords, get page() { return this.coords.page }, get client() { return this.coords.client }, get timeStamp() { return this.coords.timeStamp }, get pageX() { return this.coords.page.x }, get pageY() { return this.coords.page.y }, get clientX() { return this.coords.client.x }, get clientY() { return this.coords.client.y }, get pointerId() { return this.coords.pointerId }, get target() { return this.coords.target }, get type() { return this.coords.type }, get pointerType() { return this.coords.pointerType }, get buttons() { return this.coords.buttons }, preventDefault() {}, } return event as typeof event & PointerType & PointerEventType } export interface MockCoords { page: Point client: Point timeStamp?: number pointerId?: any target?: any type?: string pointerType?: string buttons?: number } export { pointerExtend } ================================================ FILE: packages/@interactjs/utils/raf.ts ================================================ let lastTime = 0 let request: typeof requestAnimationFrame let cancel: typeof cancelAnimationFrame function init(global: Window | typeof globalThis) { request = global.requestAnimationFrame cancel = global.cancelAnimationFrame if (!request) { const vendors = ['ms', 'moz', 'webkit', 'o'] for (const vendor of vendors) { request = global[`${vendor}RequestAnimationFrame` as 'requestAnimationFrame'] cancel = global[`${vendor}CancelAnimationFrame` as 'cancelAnimationFrame'] || global[`${vendor}CancelRequestAnimationFrame` as 'cancelAnimationFrame'] } } request = request && request.bind(global) cancel = cancel && cancel.bind(global) if (!request) { request = (callback) => { const currTime = Date.now() const timeToCall = Math.max(0, 16 - (currTime - lastTime)) const token = global.setTimeout(() => { // eslint-disable-next-line n/no-callback-literal callback(currTime + timeToCall) }, timeToCall) lastTime = currTime + timeToCall return token as any } cancel = (token) => clearTimeout(token) } } export default { request: (callback: FrameRequestCallback) => request(callback), cancel: (token: number) => cancel(token), init, } ================================================ FILE: packages/@interactjs/utils/rect.ts ================================================ import type { HasGetRect, RectResolvable, Rect, Element, Point, FullRect, EdgeOptions, } from '@interactjs/core/types' import { closest, getElementRect, parentNode } from './domUtils' import extend from './extend' import is from './is' export function getStringOptionResult(value: any, target: HasGetRect, element: Node) { if (value === 'parent') { return parentNode(element) } if (value === 'self') { return target.getRect(element as Element) } return closest(element, value) } export function resolveRectLike( value: RectResolvable, target?: HasGetRect, element?: Node, functionArgs?: T, ) { let returnValue: any = value if (is.string(returnValue)) { returnValue = getStringOptionResult(returnValue, target, element) } else if (is.func(returnValue)) { returnValue = returnValue(...functionArgs) } if (is.element(returnValue)) { returnValue = getElementRect(returnValue) } return returnValue as Rect } export function toFullRect(rect: Rect): FullRect { const { top, left, bottom, right } = rect const width = rect.width ?? rect.right - rect.left const height = rect.height ?? rect.bottom - rect.top return { top, left, bottom, right, width, height } } export function rectToXY(rect: Rect | Point) { return ( rect && { x: 'x' in rect ? rect.x : rect.left, y: 'y' in rect ? rect.y : rect.top, } ) } export function xywhToTlbr>(rect: T) { if (rect && !('left' in rect && 'top' in rect)) { rect = extend({}, rect) rect.left = rect.x || 0 rect.top = rect.y || 0 rect.right = rect.right || rect.left + rect.width rect.bottom = rect.bottom || rect.top + rect.height } return rect as Rect & T } export function tlbrToXywh(rect: Rect & Partial) { if (rect && !('x' in rect && 'y' in rect)) { rect = extend({}, rect) rect.x = rect.left || 0 rect.y = rect.top || 0 rect.width = rect.width || (rect.right || 0) - rect.x rect.height = rect.height || (rect.bottom || 0) - rect.y } return rect as FullRect & Point } export function addEdges(edges: EdgeOptions, rect: Rect, delta: Point) { if (edges.left) { rect.left += delta.x } if (edges.right) { rect.right += delta.x } if (edges.top) { rect.top += delta.y } if (edges.bottom) { rect.bottom += delta.y } rect.width = rect.right - rect.left rect.height = rect.bottom - rect.top } ================================================ FILE: packages/@interactjs/utils/shallowEqual.ts ================================================ export default function shallowEqual(left: any, right: any) { if (left === right) { return true } if (!left || !right) { return false } const leftKeys = Object.keys(left) if (leftKeys.length !== Object.keys(right).length) { return false } for (const key of leftKeys) { if (left[key] !== right[key]) { return false } } return true } ================================================ FILE: packages/@interactjs/utils/window.ts ================================================ import isWindow from './isWindow' export let realWindow = undefined as Window let win = undefined as Window export { win as window } export function init(window: Window & { wrap?: (...args: any[]) => any }) { // get wrapped window if using Shadow DOM polyfill realWindow = window // create a TextNode const el = window.document.createTextNode('') // check if it's wrapped by a polyfill if (el.ownerDocument !== window.document && typeof window.wrap === 'function' && window.wrap(el) === el) { // use wrapped window window = window.wrap(window) } win = window } if (typeof window !== 'undefined' && !!window) { init(window) } export function getWindow(node: any) { if (isWindow(node)) { return node } const rootNode = node.ownerDocument || node return rootNode.defaultView || win.window } ================================================ FILE: packages/interactjs/.npmignore ================================================ *.ts !*.d.ts *.spec.ts *.spec.js dist/docs guide ================================================ FILE: packages/interactjs/LICENSE ================================================ Copyright (c) 2012-present Taye Adeyemi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/interactjs/README.md ================================================ interact.js

JavaScript drag and drop, resizing and multi-touch gestures with inertia and snapping for modern browsers (and also IE9+).

Gitter jsDelivr Build Status

Features include: - **inertia** and **snapping** - **multi-touch**, simultaneous interactions - cross browser and device, supporting the **desktop and mobile** versions of Chrome, Firefox and Opera as well as **Internet Explorer 9+** - interaction with [**SVG**](http://interactjs.io/#use_in_svg_files) elements - being **standalone and customizable** - **not modifying the DOM** except to change the cursor (but you can disable that) Installation ------------ * [npm](https://www.npmjs.org/): `npm install interactjs` * [jsDelivr CDN](https://cdn.jsdelivr.net/npm/interactjs/): `` * [unpkg CDN](https://unpkg.com/interactjs/): `` * [Rails 5.1+](https://rubyonrails.org/): 1. `yarn add interactjs` 2. `//= require interactjs/interact` * [Webjars SBT/Play 2](https://www.webjars.org/): `libraryDependencies ++= Seq("org.webjars.npm" % "interactjs" % version)` ### Typescript definitions The project is written in Typescript and the npm package includes the type definitions, but if you need the typings alone, you can install them with: ``` npm install --save-dev @interactjs/types ``` Documentation ------------- http://interactjs.io/docs Example ------- ```javascript var pixelSize = 16; interact('.rainbow-pixel-canvas') .origin('self') .draggable({ modifiers: [ interact.modifiers.snap({ // snap to the corners of a grid targets: [ interact.snappers.grid({ x: pixelSize, y: pixelSize }), ], }) ], listeners: { // draw colored squares on move move: function (event) { var context = event.target.getContext('2d'), // calculate the angle of the drag direction dragAngle = 180 * Math.atan2(event.dx, event.dy) / Math.PI; // set color based on drag angle and speed context.fillStyle = 'hsl(' + dragAngle + ', 86%, ' + (30 + Math.min(event.speed / 1000, 1) * 50) + '%)'; // draw squares context.fillRect(event.pageX - pixelSize / 2, event.pageY - pixelSize / 2, pixelSize, pixelSize); } } }) // clear the canvas on doubletap .on('doubletap', function (event) { var context = event.target.getContext('2d'); context.clearRect(0, 0, context.canvas.width, context.canvas.height); }); function resizeCanvases () { [].forEach.call(document.querySelectorAll('.rainbow-pixel-canvas'), function (canvas) { canvas.width = document.body.clientWidth; canvas.height = window.innerHeight * 0.7; }); } // interact.js can also add DOM event listeners interact(document).on('DOMContentLoaded', resizeCanvases); interact(window).on('resize', resizeCanvases); ``` See the above code in action at https://codepen.io/taye/pen/tCKAm License ------- interact.js is released under the [MIT License](http://taye.mit-license.org). [ijs-twitter]: https://twitter.com/interactjs [upcoming-changes]: https://github.com/taye/interact.js/blob/main/CHANGELOG.md#upcoming-changes ================================================ FILE: packages/interactjs/bower.json ================================================ { "name": "interactjs", "main": "index.js", "license": "SEE LICENSE AT https://interactjs.io/license", "description": "Drag and drop, resizing and multi-touch gestures with inertia and snapping for modern browsers (and also IE9+)", "homepage": "http://interactjs.io", "authors": [ { "name": "Taye Adeyemi", "email": "dev@taye.me", "url": "http://taye.me" } ], "keywords": [ "interact.js", "draggable", "droppable", "drag", "drop", "drag and drop", "resize", "touch", "multi-touch", "gesture", "snap", "inertia", "grid", "autoscroll", "SVG" ], "moduleType": [ "amd", "globals", "node" ], "ignore": [ "/*", "!src", "!dist", "!LICENSE" ] } ================================================ FILE: packages/interactjs/index.ts ================================================ // eslint-disable-next-line import/no-extraneous-dependencies import interact from '@interactjs/interactjs' export default interact if (typeof module === 'object' && !!module) { try { module.exports = interact } catch {} } ;(interact as any).default = interact ================================================ FILE: packages/interactjs/package.json ================================================ { "name": "interactjs", "version": "1.10.27", "main": "dist/interact.min.js", "typings": "index.d.ts", "description": "Drag and drop, resizing and multi-touch gestures with inertia and snapping for modern browsers (and also IE9+)", "homepage": "https://interactjs.io", "authors": [ { "name": "Taye Adeyemi", "email": "dev@taye.me", "url": "https://taye.me" } ], "repository": { "type": "git", "url": "https://github.com/taye/interact.js.git" }, "keywords": [ "interact.js", "draggable", "droppable", "drag", "drop", "drag and drop", "resize", "touch", "multi-touch", "gesture", "snap", "inertia", "grid", "autoscroll", "SVG", "interact" ], "scripts": { "test": "cd ../; npm test" }, "dependencies": { "@interactjs/types": "1.10.27" }, "sideEffects": [ "**/index.js", "**/index.prod.js" ], "license": "MIT" } ================================================ FILE: scripts/.eslintrc.cjs ================================================ module.exports = { extends: '../.eslintrc.cjs', parserOptions: { sourceType: 'script', ecmaVersion: 2018 }, rules: { 'import/no-extraneous-dependencies': 'off' }, } ================================================ FILE: scripts/addPluginIndexes.js ================================================ const fs = require('fs').promises const path = require('path') const { mkdirp } = require('mkdirp') module.exports = (plugins) => { return Promise.all( plugins.map(async (modulePath) => { const [scopePath] = modulePath.split('/') const packagePath = path.join('packages', '@interactjs', scopePath) const pluginPath = path.join('packages', '@interactjs', modulePath) const dest = path.join(packagePath, path.dirname(path.relative(packagePath, pluginPath)), 'index.ts') const destDir = path.dirname(dest) const pluginSpecifier = pluginPath.replace(/^packages./, '') await mkdirp(destDir) await fs.writeFile( dest, [ '/* eslint-disable no-console, eol-last, import/no-duplicates, import/no-extraneous-dependencies, import/order */', `import '${pluginSpecifier}'`, "import interact from '@interactjs/interact/index'", `import plugin from '${pluginSpecifier}'`, 'interact.use(plugin)', ].join('\n'), ) console.log(`wrote ${dest}`) }), ) } ================================================ FILE: scripts/babel/absolute-imports.js ================================================ const path = require('path') const resolveSync = require('resolve').sync const { getModuleDirectories, shouldIgnoreImport, getRelativeToRoot } = require('../utils') module.exports = function transformImportsToAbsolute() { const fixImportSource = ({ node: { source } }, { opts, filename }) => { if (!source || (opts.ignore && opts.ignore(filename, source.value))) return const { moduleDirectory = getModuleDirectories() } = opts if (shouldIgnoreImport(source.value)) return const { extension = '', prefix } = opts const basedir = path.dirname(filename) let resolvedImport = '' resolvedImport = resolveSync(source.value, { extensions: ['.ts', '.tsx', '.js'], basedir, moduleDirectory, }) try { const unrootedImport = getRelativeToRoot(resolvedImport, moduleDirectory, prefix).result source.value = extension === null ? unrootedImport : unrootedImport.replace(/\.[jt]sx?$/, extension) } catch (error) { source.value = resolveSync(source.value, { basedir, moduleDirectory, }) } } return { name: '@interactjs/_dev:absolute-imports', visitor: { ImportDeclaration: fixImportSource, ExportNamedDeclaration: fixImportSource, }, } } ================================================ FILE: scripts/babel/inline-env-vars.js ================================================ module.exports = function transformInlineEnvironmentVariables({ types: t }) { return { name: '@interactjs/_dev:inline-env-vars', visitor: { // eslint-disable-next-line no-shadow MemberExpression(path, { opts: { include, exclude, env } = {} }) { if (path.get('object').matchesPattern('process.env')) { const key = path.toComputedKey() if ( t.isStringLiteral(key) && (!include || include.indexOf(key.value) !== -1) && (!exclude || exclude.indexOf(key.value) === -1) ) { const name = key.value const value = env && name in env ? env[name] : process.env[name] path.replaceWith(t.valueToNode(value)) } } }, }, } } ================================================ FILE: scripts/babel/relative-imports.js ================================================ const path = require('path') const resolveSync = require('resolve').sync const { getModuleDirectories, shouldIgnoreImport, getRelativeToRoot } = require('../utils') module.exports = function transformImportsToRelative() { const fixImportSource = ({ node: { source } }, { opts, filename }) => { if (!source || (opts.ignore && opts.ignore(filename))) return const { moduleDirectory = getModuleDirectories() } = opts if (shouldIgnoreImport(source.value, filename, moduleDirectory)) return const { extension = '.js' } = opts const basedir = path.dirname(getRelativeToRoot(filename, moduleDirectory).result) let resolvedImport = '' for (const root of moduleDirectory) { try { resolvedImport = resolveSync(source.value, { extensions: ['.ts', '.tsx'], basedir: path.join(root, basedir), moduleDirectory, }) break } catch {} } if (!resolvedImport) { throw new Error(`Couldn't find module "${source.value}" from "${filename}"`) } const relativeImport = path.relative(basedir, getRelativeToRoot(resolvedImport, moduleDirectory).result) const importWithDir = /^[./\\]/.test(relativeImport) ? relativeImport : `${path.sep}${relativeImport}` source.value = importWithDir.replace(/^\//, `.${path.sep}`).replace(/\.tsx?$/, extension) } return { name: '@interactjs/_dev:relative-imports', visitor: { ImportDeclaration: fixImportSource, ExportNamedDeclaration: fixImportSource, ExportAllDeclaration: fixImportSource, }, } } ================================================ FILE: scripts/babel/vue-sfc.js ================================================ const { parse, compileScript, compileStyle } = require('@vue/compiler-sfc') const hash = require('hash-sum') module.exports = function transformVueSfc() { return { name: '@interactjs/_dev:vue-sfc', parserOverride(source, options, babelParse) { const { sourceFileName, filename = sourceFileName } = options if (!filename?.endsWith('.vue')) return const { code, map } = compileSfc(source, { filename, isProd: true }) const newFilename = filename + '.ts' return babelParse(code, { ...options, inputSourceMap: map, sourceFileName: newFilename, filename: newFilename, }) }, } } function compileSfc(source, { filename, isProd = true }) { const id = hash([filename, source].join('\0')) const { descriptor: sfc, errors: parseErrors } = parse(source, { filename, sourceMap: !isProd, }) if (parseErrors.length) throw parseErrors const script = compileScript(sfc, { id, inlineTemplate: true, isProd }) const styles = sfc.styles.map((style) => compileStyle({ source: style.content, filename, id, scoped: style.attrs.scoped, isProd, }), ) return { code: `${script.content}\n;${getStyleStatement(styles)}`, map: script.map, } } function getStyleStatement(styles) { if (!styles.length) return '' const css = styles.map((style) => style.code).join('\n') // TODO: minify CSS const html = `` return ['document.head.insertAdjacentHTML(', '"beforeEnd",', JSON.stringify(html), ')'].join('') } module.exports.compileSfc = compileSfc ================================================ FILE: scripts/bin/_check_deps.js ================================================ const fs = require('fs/promises') const { getPackageJsons, errorExit } = require('../utils') async function checkDeps () { const packageJsons = await getPackageJsons() const pkgNames = new Set(packageJsons.map(([, pkg]) => pkg.name)) Promise.all( packageJsons.map(async ([p, pkg]) => { for (const depField of ['dependencies', 'peerDependencies', 'devDependencies']) { const missingDeps = Object.keys(pkg[depField] || {}).filter( (depName) => depName.startsWith('@interactjs/') && !pkgNames.has(depName), ) for (const depName of missingDeps) { delete pkg[depField][depName] console.warn(`tidying ${pkg.name} ${depField} ✕ ${depName}`) } } await fs.writeFile(p, JSON.stringify(pkg, null, 2) + '\n') }), ) } checkDeps().catch(errorExit) ================================================ FILE: scripts/bin/add_plugin_indexes.js ================================================ const { isPro } = require('../utils') require('../addPluginIndexes')([ 'actions/plugin', 'actions/drag/plugin', 'actions/drop/plugin', 'actions/resize/plugin', 'actions/gesture/plugin', 'auto-scroll/plugin', 'auto-start/plugin', 'dev-tools/plugin', 'inertia/plugin', 'modifiers/plugin', 'pointer-events/plugin', 'reflow/plugin', 'snappers/plugin', ...(isPro ? [ 'react/plugin', 'vue/plugin', 'multi-target/plugin', 'feedback/plugin', 'clone/plugin', 'arrange/plugin', 'iframes/plugin', ] : []), ]) ================================================ FILE: scripts/bin/bundle.js ================================================ const path = require('path') const bundler = require('../bundler') const headers = require('../headers') const { errorExit } = require('../utils') const [, , entry = 'packages/interactjs', name = 'interact'] = process.argv const entryPkgDir = path.join(process.cwd(), entry) const options = { headers, entry: path.join(entryPkgDir, 'index.ts'), destDir: path.join(entryPkgDir, 'dist'), name, } process.stdout.write('Bundling...') bundler(options) .then(async (code) => console.log(' done.')) .catch(errorExit) ================================================ FILE: scripts/bin/clean.js ================================================ const fs = require('fs') const path = require('path') const shell = require('shelljs') const { getBuiltJsFiles } = require('../utils') console.log('removing typescript generated files.') shell.exec('tsc -b types.tsconfig.json --clean') Promise.all([getBuiltJsFiles(), import('del').then((m) => m.deleteAsync)]).then(async ([filenames, del]) => { console.log(`removing ${filenames.length} generated files and directories.`) await Promise.all( filenames.map((filename) => { return del(filename) }), ) // remove empty directories const directories = [...new Set(filenames.map(path.dirname))].sort().reverse() for (const dir of directories) { const files = await fs.promises.readdir(dir) if (!files.length) { await del(dir) } } }) ================================================ FILE: scripts/bin/lint.js ================================================ const { existsSync, promises: fs } = require('fs') const { ESLint } = require('eslint') const { glob } = require('glob') const prettier = require('prettier') const yargs = require('yargs') const { lintSourcesGlob, lintIgnoreGlobs, errorExit } = require('../utils') const { fix, _: fileArgs } = yargs.boolean('fix').argv const jsExt = /\.js$/ const dtsExt = /\.d\.ts$/ main().catch(errorExit) async function main() { const sources = fileArgs.length ? fileArgs : await getSources() console.log(`Linting ${sources.length} 'file${sources.length === 1 ? '' : 's'}...`) if (fix) { await Promise.all(sources.map(formatWithPrettier)) } const eslint = new ESLint({ fix, useEslintrc: true, }) const results = await eslint.lintFiles(sources) const formatter = await eslint.loadFormatter('stylish') if (fix) { await ESLint.outputFixes(results) } console.log(formatter.format(results)) const hasUnfixedError = results.some((r) => r.errorCount > (fix ? r.fixableErrorCount : 0)) if (hasUnfixedError) { throw new Error('unfixed errors remain') } } async function formatWithPrettier(filepath) { const [source, config] = await Promise.all([ fs.readFile(filepath).then((buffer) => buffer.toString()), prettier.resolveConfig(filepath), ]) const output = await prettier.format(source, { ...config, filepath }) if (source !== output) await fs.writeFile(filepath, output) } async function getSources() { const sources = await glob(lintSourcesGlob, { ignore: lintIgnoreGlobs, silent: true, }) return sources.filter((source) => !isGenerated(source)) } function isGenerated(source) { return ( (dtsExt.test(source) && existsSync(source.replace(dtsExt, '.ts'))) || (jsExt.test(source) && existsSync(source.replace(jsExt, '.ts'))) ) } ================================================ FILE: scripts/bin/release.js ================================================ const fs = require('fs').promises const path = require('path') const shell = require('shelljs') const { getPackages, isPro, registryUrl, errorExit } = require('../utils') const cwd = process.cwd() process.env.PATH = `${cwd}/bin:${cwd}/node_modules/.bin:${process.env.PATH}` shell.config.verbose = true shell.config.fatal = true ensureCleanIndex() const { gitTag } = checkVersion() let packages main().catch(errorExit) async function main(ps) { configGitUser() gitDetatch() clean() packages = await getPackages() await runBuild() await commit() await pushAndPublish() } function configGitUser() { shell.exec('git config user.name "CI"') shell.exec('git config user.email "<>"') } function ensureCleanIndex() { // make sure the repo is clean try { shell.exec('git diff-index -G . HEAD --stat --exit-code') } catch { throw new Error('working directory must be clean') } } function checkVersion() { const getVersion = require('../getVersion') const version = require('semver').clean(getVersion()) if (!version) { throw new Error('failed to parse version') } return { version, gitTag: 'v' + version, } } function gitDetatch() { shell.exec('git checkout --detach') } function clean() { shell.exec('_clean') } async function runBuild() { // copy README to interactjs package await Promise.all( packages .filter((p) => p.endsWith('interactjs')) .map((p) => fs.copyFile(`${cwd}/README.md`, `${p}/README.md`)), ) // copy license file and npmignore to all packages const licenseFilename = isPro ? 'LICENSE.md' : 'LICENSE' await Promise.all( packages.map(async (pkg) => { await fs.copyFile(licenseFilename, path.join(pkg, licenseFilename)) await fs.copyFile('.npmignore', path.join(pkg, '.npmignore')) }), ) if (isPro) await fs.rm(path.resolve('LICENSE')) // clean up scope deps shell.exec('npx _check_deps') if (!isPro) { // bundle interactjs shell.exec('npm run build:bundle') // ensure that the output is valid ES5 syntax shell.exec('acorn --silent --ecma5 packages/interactjs/dist/*.js') // generate docs shell.exec('npm run build:docs') } // create @interactjs/**/use/* modules shell.exec('npx _add_plugin_indexes') // generate types shell.exec('npx _types') // generate esnext .js modules shell.exec('rollup -c esnext.rollup.config.cjs') // ensure that the output is valid ES2018 syntax shell.exec('acorn --silent --module --ecma2018 packages/**/*.js') // set publishConfig await editPackageJsons((pkg) => { pkg.publishConfig = isPro ? { access: 'restricted', registry: registryUrl } : { access: 'public' } }) } function commit() { // commit and add new version tag shell.exec('git add --all .') shell.exec('git add --force packages') if (!isPro) shell.exec('git add --force dist/api') shell.exec('git reset **/node_modules') shell.exec(`git commit --no-verify -m ${gitTag}`) } async function pushAndPublish() { const { NPM_TAG } = process.env try { shell.exec(`git push --no-verify origin HEAD:refs/tags/${gitTag}`) } catch { throw new Error(`failed to push git tag ${gitTag} to origin`) } const gitHead = shell.exec('git rev-parse --short HEAD').trim() await editPackageJsons((pkg) => { pkg.gitHead = gitHead }) const { deleteAsync } = await import('del') if (isPro) await deleteAsync('packages/**/*.map') const npmPublishCommand = 'npm publish' + (NPM_TAG ? ` --tag ${NPM_TAG}` : '') const packagesToPublish = isPro ? packages.filter((p) => /@interactjs\//.test(p)) : packages for (const pkg of packagesToPublish) { shell.exec(npmPublishCommand, { cwd: path.resolve(pkg) }) } shell.exec('git checkout $(git ls-files "**package.json")') } async function editPackageJsons(func) { await Promise.all( ['.', ...packages].map(async (packageDir) => { const file = path.resolve(packageDir, 'package.json') const pkg = JSON.parse((await fs.readFile(file)).toString()) func(pkg) await fs.writeFile(file, `${JSON.stringify(pkg, null, 2)}\n`) }), ) } ================================================ FILE: scripts/bin/types.js ================================================ const path = require('path') const shell = require('shelljs') const execTypes = require('../execTypes') const { errorExit } = require('../utils') shell.config.verbose = true shell.config.fatal = true const typesDir = '@interactjs/types' ;(async () => { const modulesDir = path.resolve('packages') execTypes.modular(modulesDir) await execTypes.combined(path.join(modulesDir, typesDir)) })().catch(errorExit) ================================================ FILE: scripts/bin/version.js ================================================ const fs = require('fs') const path = require('path') const { glob } = require('glob') const semver = require('semver') const getVersion = require('../getVersion') let [, , cwd, versionChange, prereleaseId] = process.argv if (cwd === undefined || !cwd.startsWith('/')) { ;[cwd, versionChange, prereleaseId] = [process.cwd(), cwd, versionChange] } const depFields = ['dependencies', 'peerDependencies', 'devDependencies', 'optionalDependencies'] let currentVersion const previousVersion = getVersion(cwd) if (versionChange) { if (/^(major|minor|patch|premajor|preminor|prepatch|prerelease)$/.test(versionChange)) { currentVersion = semver.inc(previousVersion, versionChange, prereleaseId) } else { currentVersion = semver.clean(versionChange) if (currentVersion === null) { throw Error(`Invalid version change "${previousVersion}" -> "${versionChange}"`) } } const versionTable = [] for (const file of [ 'package.json', ...glob.sync('packages/{@interactjs/*,interactjs}/package.json', { cwd }), ]) { const pkg = require(path.resolve(file)) versionTable.push({ package: pkg.name, old: pkg.version, new: currentVersion }) pkg.version = currentVersion for (const deps of depFields.map((f) => pkg[f]).filter(Boolean)) { for (const name of Object.keys(deps).filter((n) => /@?interactjs\//.test(n))) { if (deps[name] === previousVersion) { deps[name] = currentVersion } else { console.warn(`${file}: not updating "${name}" from "${deps[name]}"`) } } } fs.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`) } console.table(versionTable) } // if this was run with no arguments, get the current version else { currentVersion = previousVersion console.log(currentVersion) } ================================================ FILE: scripts/execTypes.js ================================================ const fs = require('fs') const path = require('path') const shell = require('shelljs') module.exports = { modular(modulesDir) { shell.exec(`npx tsc -p types.tsconfig.json --outDir ${modulesDir}/@interactjs`) }, async combined(outDir) { const outFile = path.join(outDir, 'index.d.ts') // await del(path.join(typesOutDir, outBasename)) shell.exec(`npx tsc -p types.tsconfig.json --rootDir packages --outFile ${outFile}`) const namespaceDeclaration = ` import * as Interact from '@interactjs/types/index' export as namespace Interact export = Interact `.trimStart() await fs.promises.writeFile(path.join(outDir, 'typings.d.ts'), namespaceDeclaration) }, } ================================================ FILE: scripts/getVersion.js ================================================ const path = require('path') module.exports = (cwd = process.cwd()) => { const rootPkg = require(path.resolve(cwd, 'package.json')) return rootPkg.version } ================================================ FILE: scripts/headers.js ================================================ const version = require('../scripts/getVersion')() module.exports = process.env.INTERACTJS_TIER === 'pro' ? { raw: `/** * interact.js ${version} * * Copyright (c) 2012-present Taye Adeyemi * https://interactjs.io/license */\n`, min: `/* interact.js ${version} | https://interactjs.io/license */\n`, } : { raw: `/** * interact.js ${version} * * Copyright (c) 2012-present Taye Adeyemi * Released under the MIT License. * https://raw.github.com/taye/interact.js/main/LICENSE */\n`, min: `/* interact.js ${version} | https://raw.github.com/taye/interact.js/main/LICENSE */\n`, } ================================================ FILE: scripts/utils.js ================================================ const fs = require('fs') const path = require('path') const { glob } = require('glob') const resolveSync = require('resolve').sync const sourcesGlob = 'packages/{,@}interactjs/**/**/*{.ts,.tsx,.vue}' const lintSourcesGlob = `{${sourcesGlob},{scripts,examples,docs}/**/*.{js,cjs,md},bin/**/*}` const commonIgnoreGlobs = ['**/node_modules/**', '**/*_*', '**/*.d.ts', '**/dist/**', 'examples/js/**'] const lintIgnoreGlobs = [...commonIgnoreGlobs] const sourcesIgnoreGlobs = [...commonIgnoreGlobs, '**/*.spec.ts'] const builtFilesGlob = '{{**/dist/**,packages/{,@}interactjs/**/**/*.js{,.map}},packages/@interactjs/**/index.ts}' const builtFilesIgnoreGlobs = [ '**/node_modules/**', 'packages/@interactjs/{dev-tools/babel-plugin-prod.js,{types,interact,interactjs,rebound}/index.ts}', ] const getSources = ({ cwd = process.cwd(), ...options } = {}) => glob(sourcesGlob, { cwd, ignore: sourcesIgnoreGlobs, strict: false, nodir: true, absolute: true, ...options, }) const getBuiltJsFiles = ({ cwd = process.cwd() } = {}) => glob(builtFilesGlob, { cwd, ignore: builtFilesIgnoreGlobs, strict: false, nodir: true, }) function getEsnextBabelOptions(presetEnvOptions) { return { babelrc: false, configFile: false, sourceMaps: true, presets: [ [ require.resolve('@babel/preset-env'), { shippedProposals: true, ...presetEnvOptions, }, ], [ require.resolve('@babel/preset-typescript'), { isTSX: false, onlyRemoveTypeImports: true, allExtensions: true, allowDeclareFields: true }, ], ], plugins: [ require.resolve('./babel/vue-sfc'), require.resolve('@babel/plugin-proposal-optional-catch-binding'), require.resolve('@babel/plugin-proposal-optional-chaining'), require.resolve('@babel/plugin-transform-nullish-coalescing-operator'), require.resolve('@babel/plugin-transform-logical-assignment-operators'), ], assumptions: { iterableIsArray: true, noDocumentAll: true, noNewArrows: true, setPublicClassFields: true, }, } } function getDevPackageDir() { return path.join(__dirname, '..') } function getModuleName(tsName) { return tsName.replace(/\.[jt]sx?$/, '') } function getModuleDirectories() { return [path.join(__dirname, '..', 'packages'), path.join(process.cwd(), 'node_modules')] } async function getPackages(options) { const packageJsonPaths = await glob('packages/{@interactjs/*,interactjs}/package.json', { ignore: commonIgnoreGlobs, ...options, }) const packageDirs = packageJsonPaths.map(path.dirname) return [...new Set(packageDirs)].sort() } async function getPackageJsons(packages = getPackages()) { return Promise.all( (await packages).map(async (p) => { const jsonPath = path.resolve(p, 'package.json') const pkg = JSON.parse((await fs.promises.readFile(jsonPath)).toString()) return [jsonPath, pkg] }), ) } function shouldIgnoreImport(sourceValue) { return !/^(\.{1-2}|(@interactjs))[\\/]/.test(sourceValue) } const isPro = process.env.INTERACTJS_TIER === 'pro' const registryUrl = isPro ? 'https://registry.interactjs.io' : undefined function extendBabelOptions( { ignore = [], plugins = [], presets = [], ...others }, base = getEsnextBabelOptions(), ) { return { ...base, ...others, ignore: [...(base.ignore || []), ...ignore], presets: [...(base.presets || []), ...presets], plugins: [...(base.plugins || []), ...plugins], } } function getPackageDir(filename) { let packageDir = filename while (!fs.existsSync(path.join(packageDir, 'package.json'))) { packageDir = path.dirname(packageDir) if (packageDir === path.sep) { throw new Error(`Couldn't find a package for ${filename}`) } } return packageDir } function getRelativeToRoot(filename, moduleDirectory, prefix = '/') { filename = path.normalize(filename) const ret = withBestRoot((root) => { const valid = filename.startsWith(root) const result = valid && path.join(prefix, path.relative(root, filename)) const priority = valid && -result.length return { valid, result, priority } }, moduleDirectory) if (!ret.result) { throw new Error(`Couldn't find module ${filename} in ${moduleDirectory.join(' or')}.`) } return ret } /** * use the result of `func` most shallow valid root */ function withBestRoot(func, moduleDirectory) { const roots = moduleDirectory.map(path.normalize) return ( roots.reduce((best, root) => { const { result, valid, priority } = func(root) if (!valid) { return best } if (!best || priority > best.priority) { return { result, priority, root } } return best }, null) || {} ) } function resolveImport(specifier, basedir, moduleDirectory) { if (specifier.startsWith('.')) { specifier = path.join(basedir, specifier) } return resolveSync(specifier, { extensions: ['.ts', '.tsx'], moduleDirectory, }) } function getShims() { try { return require('../scripts/shims') } catch { return [] } } function errorExit(error) { console.error(error) process.exit(1) } module.exports = { getSources, sourcesGlob, lintSourcesGlob, commonIgnoreGlobs, sourcesIgnoreGlobs, lintIgnoreGlobs, getBuiltJsFiles, getEsnextBabelOptions, extendBabelOptions, getDevPackageDir, getPackages, getPackageJsons, getModuleName, getModuleDirectories, getPackageDir, getRelativeToRoot, withBestRoot, resolveImport, isPro, registryUrl, shouldIgnoreImport, getShims, errorExit, } ================================================ FILE: shims.d.ts ================================================ declare module '*.vue' { import type { DefineComponent } from 'vue' const component: ReturnType> export default component } ================================================ FILE: test/.eslintrc.cjs ================================================ module.exports = { extends: '../.eslintrc.cjs', env: { browser: true }, rules: { 'no-console': 2, strict: [2, 'never'] }, } ================================================ FILE: test/fixtures/babelPluginProject/index.js ================================================ // eslint-disable-next-line import/no-extraneous-dependencies export * from '@interactjs/a' ================================================ FILE: test/fixtures/babelPluginProject/node_modules/@interactjs/a/a.js ================================================ export const a = {} export * from './b' ================================================ FILE: test/fixtures/babelPluginProject/node_modules/@interactjs/a/b/b.js ================================================ export const b = {} ================================================ FILE: test/fixtures/babelPluginProject/node_modules/@interactjs/a/b/index.js ================================================ export { b } from './b' ================================================ FILE: test/fixtures/babelPluginProject/node_modules/@interactjs/a/package-main-file.js ================================================ export * from './a' ================================================ FILE: test/fixtures/babelPluginProject/node_modules/@interactjs/a/package.json ================================================ { "main": "package-main-file.js" } ================================================ FILE: test/fixtures/dependentTsProject/index.ts ================================================ /* eslint-disable import/no-extraneous-dependencies, import/no-unresolved */ import interact from 'interactjs' // Interactables interact(document.body) interact(document) interact(window) interact('.drag-and-resize') .draggable({ inertia: true, modifiers: [ interact.modifiers.snap({ targets: [ { x: 100, y: 200 }, (x: number, y: number) => ({ x: x % 20, y }), interact.snappers.grid({ x: 20, y: 0 }), ], offset: 'startCoords', relativePoints: [{ x: 0, y: 1 }], endOnly: true, }), interact.modifiers.snapSize({ targets: [ { x: 100, y: 200 }, (x: number, y: number) => ({ x: x % 20, y }), interact.snappers.grid({ width: 100, height: 500 }), ], endOnly: true, }), interact.modifiers.restrictRect({ restriction: 'parent', endOnly: true, }), interact.modifiers.restrict({ restriction: _ => ({ top: 0, left: 0, bottom: 1, right: 1 }), }), interact.modifiers.restrict({ restriction: _ => document.body, }), interact.modifiers.restrictSize({ min: document.body, max: 'parent', }), interact.modifiers.restrictEdges({ inner: document.body, outer: 'parent', }), ], }) .resizable({ inertia: true, }) // Selector context const myList = document.querySelector('#my-list') as HTMLElement | SVGElement interact('li', { context: myList, }).draggable({ /* ... */ }) // Action options const target = 'li' interact(target) .draggable({ max: 1, maxPerElement: 2, manualStart: true, modifiers: [], inertia: { /* ... */ }, autoScroll: { /* ... */ }, lockAxis: 'x' || 'y' || 'start', startAxis: 'x' || 'y', }) .resizable({ max: 1, maxPerElement: 2, manualStart: true, modifiers: [], inertia: { /* ... */ }, autoScroll: { /* ... */ }, margin: 50, square: true || false, axis: 'x' || 'y', }) .gesturable({ max: 1, maxPerElement: 2, manualStart: true, modifiers: [], }) // autoscroll const element = 'li' interact(element) .draggable({ autoScroll: true, }) .resizable({ autoScroll: { container: document.body, margin: 50, distance: 5, interval: 10, }, }) // axis interact(target) .draggable({ startAxis: 'x', lockAxis: 'y', }) .draggable({ startAxis: 'xy', lockAxis: 'x', }) interact(target).resizable({ axis: 'x', }) const handleEl = 'li' interact(target).resizable({ edges: { top: true, // Use pointer coords to check for resize. left: false, // Disable resizing from left edge. bottom: '.resize-s', // Resize if pointer target matches selector right: handleEl, // Resize if pointer target is the given Element }, }) // resize invert interact(target).resizable({ edges: { bottom: true, right: true }, invert: 'reposition', }) // resize square interact(target).resizable({ squareResize: true, }) // dropzone accept interact(target).dropzone({ accept: '.drag0, .drag1', }) // dropzone overlap interact(target).dropzone({ overlap: 0.25, }) // dropzone checker interact(target).dropzone({ checker ( _dragEvent: Interact.Element, // related dragmove or dragend _event: Event, // Touch, Pointer or Mouse Event dropped: boolean, // bool default checker result _dropzone: Interact.Interactable, // dropzone Interactable dropElement: Interact.Element, // dropzone elemnt _draggable: Interact.Interactable, // draggable Interactable _draggableElement: Interact.Element, ) { // draggable element // only allow drops into empty dropzone elements return dropped && !dropElement.hasChildNodes() }, }) interact.dynamicDrop() interact.dynamicDrop(false) // Events function listener (event: Interact.InteractEvent) { const { type, pageX, pageY } = event alert({ type, pageX, pageY }) } interact(target) .on('dragstart', listener) .on('dragmove dragend', listener) .on(['resizemove', 'resizeend'], listener) .on({ gesturestart: listener, gestureend: listener, }) interact.on('resize', (event: Interact.ResizeEvent) => { const { rect, deltaRect } = event alert(JSON.stringify({ rect, deltaRect })) }) interact(target).resizable({ listeners: [{ start: listener, move: listener }], }) interact(target).draggable({ listeners: { start: listener, end: listener }, }) interact(target).draggable({ onstart: listener, onmove: listener, onend: listener, }) interact.on(['dragmove', 'resizestart'], listener) // devTools options interact(target).devTools({ ignore: { boxSizing: true, touchAction: true }, }) const dropTarget = 'div' // Drop Events interact(dropTarget) .dropzone({ ondrop (event) { alert(event.relatedTarget.id + ' was dropped into ' + event.target.id) }, }) .on('dropactivate', event => { event.target.classList.add('drop-activated') }) interact(target).on('up', _event => {}) // fast click interact('a[href]').on('tap', event => { window.location.href = event.currentTarget.href event.preventDefault() }) ================================================ FILE: test/fixtures/dependentTsProject/tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "module": "commonjs", "lib": ["dom", "esnext"], "strict": true, "skipLibCheck": false, "esModuleInterop": true, "noEmit": true } } ================================================ FILE: tsconfig.json ================================================ { "include": [ "packages", "shims.d.ts", "scripts", "jsdoc" ], "exclude": [ "test/fixtures", "_angular", "**/dist" ], "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "node", "esModuleInterop": true, "allowJs": false, "skipLibCheck": true, "noEmit": true, "strict": false, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "jsx": "preserve", "experimentalDecorators": true, "lib": [ "esnext", "dom" ], "baseUrl": ".", "paths": { "@interactjs/_dev/*": ["./*"] } } } ================================================ FILE: typedoc.config.cjs ================================================ const { isPro } = require('./scripts/utils') /** @type {import('typedoc').TypeDocOptions} */ const config = { name: '@interactjs', entryPoints: [ 'actions', 'auto-scroll', 'auto-start', 'core', 'dev-tools', 'inertia', 'modifiers', 'pointer-events', 'reflow', 'snappers', ].map((pkg) => `./packages/@interactjs/${pkg}/*.ts`), exclude: ['**/*.{spec,stub}.ts{,x}', '**/_*'], excludeReferences: true, excludeInternal: true, excludeExternals: true, excludePrivate: true, excludeNotDocumented: true, excludeNotDocumentedKinds: [ 'Module', 'Variable', 'Function', 'Constructor', 'Method', 'IndexSignature', 'ConstructorSignature', 'Reference', ], basePath: './packages/@interactjs', titleLink: '/', readme: 'none', disableSources: isPro, plugin: ['typedoc-plugin-markdown'], out: './dist/api', } module.exports = config ================================================ FILE: types.tsconfig.json ================================================ { "include": ["packages/@interactjs", "shims.d.ts"], "exclude": ["**/*.spec.ts", "packages/@interactjs/vue", "packages/@interactjs/react"], "compilerOptions": { "target": "esnext", "module": "esnext", "lib": ["esnext", "dom"], "moduleResolution": "node", "esModuleInterop": true, "declaration": true, "declarationMap": false, "emitDeclarationOnly": true, "stripInternal": true, "skipLibCheck": true, "strict": false } } ================================================ FILE: vijest.config.js ================================================ module.exports = { launch: { executablePath: 'chromium', // devtools: true, // slowMo: 1000, }, shareBrowserContext: true, } ================================================ FILE: vite.config.ts ================================================ import path from 'path' import vue from '@vitejs/plugin-vue' import serveIndex from 'serve-index' import type { Plugin } from 'vite' import { defineConfig } from 'vite' const examplesDir = path.resolve(__dirname, 'examples') export default defineConfig({ root: examplesDir, resolve: { alias: { '@interactjs/': path.resolve(__dirname, 'packages/@interactjs'), interactjs: path.resolve(__dirname, 'packages/interactjs'), }, }, define: { ...getDefinedEnv(), }, plugins: [vue(), dirListing()], optimizeDeps: { include: ['react'], }, server: { port: 8081, }, }) function getDefinedEnv () { const entries = Object.entries(process.env) .filter(([key]) => /^(NODE_ENV|npm_package_version|INTERACTJS_.*)$/.test(key)) .map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]) return Object.fromEntries(entries) } function dirListing (): Plugin { return { name: 'dir-listing', configureServer (server) { server.middlewares.use(serveIndex(examplesDir, { icons: true }) as any) }, } }