diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..ab272ee27 --- /dev/null +++ b/.babelrc @@ -0,0 +1,19 @@ +{ + "loose": ["all"], + "whitelist": [ + "es3.memberExpressionLiterals", + "es3.propertyLiterals", + "es6.arrowFunctions", + "es6.blockScoping", + "es6.classes", + "es6.constants", + "es6.destructuring", + "es6.forOf", + "es6.literals", + "es6.objectSuper", + "es6.properties.computed", + "es6.properties.shorthand", + "es6.tailCall", + "es6.templateLiterals" + ] +} diff --git a/.eslintrc b/.eslintrc index 1a8cec412..b9d63f5a0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,5 @@ +parser: babel-eslint + extends: 'eslint:recommended' env: @@ -5,21 +7,40 @@ env: commonjs: true rules: + comma-dangle: [2, always-multiline] comma-style: [2, last] curly: 2 dot-notation: 2 eol-last: 2 eqeqeq: 2 guard-for-in: 0 + indent: [2, 2] linebreak-style: [2, unix] no-caller: 2 no-extra-bind: 2 no-self-compare: 2 no-sequences: 2 + no-shadow: 2 no-shadow-restricted-names: 2 no-trailing-spaces: 2 no-unused-expressions: 2 + no-var: 2 + one-var: [2, never] + prefer-const: 2 quotes: [2, single, avoid-escape] semi: [2, always] space-after-keywords: [2, always] space-before-function-paren: [2, always] + strict: [2, never] + +ecma-features: + arrow-functions: true + block-bindings: true + classes: true + for-of: true + destructuring: true + object-literal-computed-properties: true + object-literal-shorthand-methods: true + oobject-literal-shorthand-properties: true + super-in-functions: true + template-strings: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f06235c46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 30e75c10e..000000000 --- a/.jshintrc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "strict" : true, - "validthis": true, - "browser" : true, - "jquery" : true, - "curly" : true, - "laxbreak" : true, - "newcap" : true, - "noarg" : true, - "undef" : true, - "unused" : true, - "strict" : true, - "trailing" : true -} diff --git a/.tern-project b/.tern-project new file mode 100644 index 000000000..60f5f9bb7 --- /dev/null +++ b/.tern-project @@ -0,0 +1,11 @@ +{ + "libs": [ + "browser" + ], + "loadEagerly": [ + "./src/**/*.js" + ], + "plugins": { + "node": {} + } +} diff --git a/demo/dropzones.html b/demo/dropzones.html index 6633de794..8666a6e86 100644 --- a/demo/dropzones.html +++ b/demo/dropzones.html @@ -3,7 +3,7 @@ Highlight dropzones with interact.js - + @@ -21,4 +21,4 @@ - \ No newline at end of file + diff --git a/demo/events.html b/demo/events.html index 1d58f9adb..d7639f1f5 100644 --- a/demo/events.html +++ b/demo/events.html @@ -9,7 +9,7 @@ - + diff --git a/demo/gallery.html b/demo/gallery.html index 1a176762d..2face745f 100644 --- a/demo/gallery.html +++ b/demo/gallery.html @@ -88,7 +88,7 @@ } - + diff --git a/demo/html_svg.html b/demo/html_svg.html index 48661b60a..f44bff37f 100644 --- a/demo/html_svg.html +++ b/demo/html_svg.html @@ -5,7 +5,7 @@ interact.js demo - + diff --git a/demo/iframes-middle.html b/demo/iframes-middle.html index b58f0f6e1..acb69fcdc 100644 --- a/demo/iframes-middle.html +++ b/demo/iframes-middle.html @@ -1,7 +1,7 @@ - + diff --git a/demo/js/gallery.js b/demo/js/gallery.js index 9f173820d..2dbb56283 100644 --- a/demo/js/gallery.js +++ b/demo/js/gallery.js @@ -2,11 +2,21 @@ interact(document).on('DOMContentLoaded', function () { "use strict"; /* global interact, Modernizr */ +/* + * This demo is very broken! + */ + var preTransform = Modernizr.prefixed('transform'), snapTarget = {}; interact('#gallery .thumbnail') .draggable({ + snap: { + targets: [], + relativePoints: [ { x: 0.5, y: 0.5 } ], + endOnly: true + }, + inertia: true, onstart: function (event) { snapTarget = { x: $('#gallery .stage').width() / 2, @@ -34,7 +44,7 @@ interact('#gallery .thumbnail') var $thumb = $(event.target); // if the drag was snapped to the stage - if (event.pageX === snapTarget.x && event.pageY === snapTarget.y) { + if (event.dropzone) { $('#gallery .stage img').removeClass('active'); $('#gallery .thumbnail').removeClass('expanded') .not($thumb).css(preTransform, ''); @@ -49,19 +59,21 @@ interact('#gallery .thumbnail') $thumb.removeClass('dragging'); } }) - .origin($('#gallery')[0]) - .snap({ - mode: 'path', - // If the pointer is far enough above the bottom of the stage - // then snap to the center of the stage - paths: [function (x, y) { - if (y < $('#gallery .stage').height() * 0.7) { - return snapTarget; - } - return {}; - }], - endOnly: true - }) - //.snap(false) - .inertia(true); + .origin($('#gallery')[0]); + + interact('#gallery .stage') + .dropzone({ + accept: ' #gallery .thumbnail', + overlap: 1, + }) + .on('dragenter', function (event) { + event.draggable.draggable({ + snap: { targets: [snapTarget] } + }); + }) + .on('dragleave drop', function (event) { + event.draggable.draggable({ + snap: { targets: [] } + }); + }); }()); diff --git a/demo/js/html_svg.js b/demo/js/html_svg.js index 983e16fef..0b72ac4ed 100644 --- a/demo/js/html_svg.js +++ b/demo/js/html_svg.js @@ -9,7 +9,8 @@ var svg, svgNS = 'http://www.w3.org/2000/svg', - SVGElement = window.SVGElement; + SVGElement = window.SVGElement, + eventTypes = interact.debug().eventTypes; function DemoGraphic(id) { var width = window.innerWidth, @@ -86,7 +87,7 @@ textProp = 'textContent'; nl = '\n'; - if ( target.demo && indexOf(interact.eventTypes, e.type) !== -1 ) { + if ( target.demo && indexOf(eventTypes, e.type) !== -1 ) { target.text[textProp] = nl + e.type; target.text[textProp] += nl + ' x0, y0 : (' + e.x0 + ', ' + e.y0 + ')'; target.text[textProp] += nl + ' dx, dy : (' + e.dx + ', ' + e.dy + ')'; @@ -232,8 +233,8 @@ interact('div.demo-node, .demo-node ellipse') .draggable({ max: 2, - autoScroll: { enabled: true }, - inertia: { enabled: true } + autoScroll: true, + inertia: true }) .gesturable({ max: 1 }) .resizable({ diff --git a/demo/js/iframes.js b/demo/js/iframes.js index d55e3bc48..c77790f05 100644 --- a/demo/js/iframes.js +++ b/demo/js/iframes.js @@ -2,15 +2,15 @@ function setInteractables () { 'use strict'; interact('.draggable', { context: document }) - .autoScroll(true) .draggable({ onmove: onMove, - }) - .inertia(true) - .restrict({ - drag: "parent", - endOnly: true, - elementRect: { top: 0, left: 0, bottom: 1, right: 1 } + inertia: { enabled: true }, + restrict: { + drag: "parent", + endOnly: true, + elementRect: { top: 0, left: 0, bottom: 1, right: 1 } + }, + autoScroll: true }); function onMove (event) { diff --git a/demo/js/snap.js b/demo/js/snap.js index 6c5d1077b..62cc892d7 100644 --- a/demo/js/snap.js +++ b/demo/js/snap.js @@ -228,14 +228,14 @@ interact(canvas) .draggable({ + inertia: { enabled: status.inertia.checked }, 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 } - }) - .inertia(status.inertia.checked); + }); if (!status.relative.checked) { snapOffset.x = snapOffset.y = 0; diff --git a/demo/snap.html b/demo/snap.html index 232322335..6e638e7e5 100644 --- a/demo/snap.html +++ b/demo/snap.html @@ -5,7 +5,7 @@ interact.js drag snapping - + diff --git a/demo/star.svg b/demo/star.svg index 739d794f7..a000502b7 100644 --- a/demo/star.svg +++ b/demo/star.svg @@ -1,7 +1,7 @@ - +
-

{{=item[0].name}}

+

{{=item[0].name}}

diff --git a/gulp/LICENSE.md b/gulp/LICENSE.md new file mode 100644 index 000000000..6879275d9 --- /dev/null +++ b/gulp/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Daniel Tello + +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. diff --git a/gulp/config.js b/gulp/config.js new file mode 100644 index 000000000..80e8e3550 --- /dev/null +++ b/gulp/config.js @@ -0,0 +1,35 @@ +var dest = "./dist"; +var src = './src'; + +module.exports = { + dest: dest, + browserSync: { + server: { + // Serve up our build folder + baseDir: dest + } + }, + markup: { + src: src + "/htdocs/**", + dest: dest + }, + browserify: { + // A separate bundle will be generated for each + // bundle config in the list below + bundleConfigs: [ + { + entries: src + '/index.js', + dest: dest, + debug: true, + outputName: 'interact.js', + outputNameMin: 'interact.min.js', + souremapComment: true, + + standalone: 'interact', + transform: [[ 'babelify', {} ]], + // Additional file extentions to make optional + extensions: [], + }, + ], + } +}; diff --git a/gulp/tasks/browserSync.js b/gulp/tasks/browserSync.js new file mode 100644 index 000000000..b1218e4b4 --- /dev/null +++ b/gulp/tasks/browserSync.js @@ -0,0 +1,7 @@ +var browserSync = require('browser-sync'); +var gulp = require('gulp'); +var config = require('../config').browserSync; + +gulp.task('browserSync', function() { + browserSync(config); +}); diff --git a/gulp/tasks/browserify.js b/gulp/tasks/browserify.js new file mode 100644 index 000000000..2ca815b1c --- /dev/null +++ b/gulp/tasks/browserify.js @@ -0,0 +1,108 @@ +/* browserify task + --------------- + Bundle javascripty things with browserify! + + This task is set up to generate multiple separate bundles, from + different sources, and to use Watchify when run from the default task. + + See browserify.bundleConfigs in gulp/config.js + */ + +'use strict'; + +var browserify = require('browserify'); +var browserSync = require('browser-sync'); +var watchify = require('watchify'); +var mergeStream = require('merge-stream'); +var bundleLogger = require('../util/bundleLogger'); +var gulp = require('gulp'); +var gulpUtil = require('gulp-util'); +var handleErrors = require('../util/handleErrors'); +var source = require('vinyl-source-stream'); +var config = require('../config').browserify; +var _ = require('lodash'); +var uglify = require('gulp-uglify'); +var buffer = require('vinyl-buffer'); +var sourcemaps = require('gulp-sourcemaps'); +var exorcist = require('exorcist'); +var rename = require('gulp-rename'); +var path = require('path'); + +var browserifyTask = function (devMode) { + + var browserifyThis = function (bundleConfig) { + + if (devMode) { + // Add watchify args and debug (sourcemaps) option + _.extend(bundleConfig, watchify.args); + // A watchify require/external bug that prevents proper recompiling, + // so (for now) we'll ignore these options during development. Running + // `gulp browserify` directly will properly require and externalize. + bundleConfig = _.omit(bundleConfig, ['external', 'require']); + } + + var b = browserify(bundleConfig); + + var bundle = function () { + // Log when bundling starts + bundleLogger.start(bundleConfig.outputName); + + return b + .bundle() + // Report compile errors + .on('error', handleErrors) + // Use vinyl-source-stream to make the + // stream gulp compatible. Specify the + // desired output filename here. + .pipe(exorcist(path.join(bundleConfig.dest, bundleConfig.outputName + '.map'), + undefined, + '', + './')) + .pipe(source(bundleConfig.outputName)) + .pipe(gulp.dest(bundleConfig.dest)) + .pipe(buffer()) + .pipe(sourcemaps.init({loadMaps: true})) + .pipe(uglify()) + .on('error', gulpUtil.log) + .pipe(rename(bundleConfig.outputNameMin)) + .pipe(sourcemaps.write('./')) + .pipe(gulp.dest(bundleConfig.dest)) + .pipe(browserSync.reload({ + stream: true + })); + }; + + if (devMode) { + // Wrap with watchify and rebundle on changes + b = watchify(b); + // Rebundle on update + b.on('update', bundle); + bundleLogger.watch(bundleConfig.outputName); + } else { + // Sort out shared dependencies. + // b.require exposes modules externally + if (bundleConfig.require) { + b.require(bundleConfig.require); + } + + // b.external excludes modules from the bundle, and expects + // they'll be available externally + if (bundleConfig.external) { + b.external(bundleConfig.external); + } + } + + return bundle(); + }; + + // Start bundling with Browserify for each bundleConfig specified + return mergeStream.apply(gulp, _.map(config.bundleConfigs, browserifyThis)); + +}; + +gulp.task('browserify', function () { + return browserifyTask(); +}); + +// Exporting the task so we can call it directly in our watch task, with the 'devMode' option +module.exports = browserifyTask; diff --git a/gulp/tasks/default.js b/gulp/tasks/default.js new file mode 100644 index 000000000..bfa677c77 --- /dev/null +++ b/gulp/tasks/default.js @@ -0,0 +1,11 @@ +var gulp = require('gulp'); +var mkdirp = require('mkdirp'); +var config = require('../config'); + +gulp.task('mkdest', function () { + mkdirp.sync(config.dest, 0755); +}); + +gulp.task('build', ['lint', 'mkdest', 'browserify']); + +gulp.task('default', ['build']); diff --git a/gulp/tasks/docs.js b/gulp/tasks/docs.js new file mode 100644 index 000000000..5445128f2 --- /dev/null +++ b/gulp/tasks/docs.js @@ -0,0 +1,5 @@ +var gulp = require('gulp'); + +gulp.task('docs', module.exports = function () { + require('child_process').execSync('./node_modules/.bin/dr.js docs/dr.json'); +}); diff --git a/gulp/tasks/karma.js b/gulp/tasks/karma.js new file mode 100644 index 000000000..1daef89e6 --- /dev/null +++ b/gulp/tasks/karma.js @@ -0,0 +1,23 @@ +var gulp = require('gulp'); +var karma = require('karma'); + +var karmaTask = function(done) { + new karma.Server({ + configFile: process.cwd() + '/karma.conf.js', + singleRun: true + }, done).start(); +}; + +var karmaContinuosTask = function(done) { + new karma.Server({ + configFile: process.cwd() + '/karma.conf.js', + action: 'watch' + }, done).start(); +}; + + +gulp.task('karma', karmaTask); + +gulp.task('test', karmaContinuosTask); + +module.exports = karmaTask; diff --git a/gulp/tasks/lint.js b/gulp/tasks/lint.js new file mode 100644 index 000000000..1aa55e00d --- /dev/null +++ b/gulp/tasks/lint.js @@ -0,0 +1,15 @@ +var gulp = require('gulp'); +var eslint = require('gulp-eslint'); + +gulp.task('lint', module.exports = function () { + return gulp.src(['src/**/*.js']) + // eslint() attaches the lint output to the eslint property + // of the file object so it can be used by other modules. + .pipe(eslint()) + // eslint.format() outputs the lint results to the console. + // Alternatively use eslint.formatEach() (see Docs). + .pipe(eslint.format()) + // To have the process exit with an error code (1) on + // lint error, return the stream and pipe to failOnError last. + // .pipe(eslint.failOnError()); +}); diff --git a/gulp/tasks/markup.js b/gulp/tasks/markup.js new file mode 100644 index 000000000..e43c6210c --- /dev/null +++ b/gulp/tasks/markup.js @@ -0,0 +1,9 @@ +var gulp = require('gulp'); +var config = require('../config').markup; +var browserSync = require('browser-sync'); + +gulp.task('markup', function() { + return gulp.src(config.src) + .pipe(gulp.dest(config.dest)) + .pipe(browserSync.reload({stream:true})); +}); diff --git a/gulp/tasks/watch.js b/gulp/tasks/watch.js new file mode 100644 index 000000000..dedcda316 --- /dev/null +++ b/gulp/tasks/watch.js @@ -0,0 +1,11 @@ +/* Notes: + - gulp/tasks/browserify.js handles js recompiling with watchify + - gulp/tasks/browserSync.js watches and reloads compiled files +*/ + +var gulp = require('gulp'); +var config = require('../config'); + +gulp.task('watch', ['watchify', /* 'karma' */], function() { + gulp.watch('./src/**/*.js', ['lint']); +}); diff --git a/gulp/tasks/watchify.js b/gulp/tasks/watchify.js new file mode 100644 index 000000000..ba032fd1c --- /dev/null +++ b/gulp/tasks/watchify.js @@ -0,0 +1,7 @@ +var gulp = require('gulp'); +var browserifyTask = require('./browserify'); + +gulp.task('watchify', function() { + // Start browserify task with devMode === true + return browserifyTask(true); +}); diff --git a/gulp/util/bundleLogger.js b/gulp/util/bundleLogger.js new file mode 100644 index 000000000..ec6e61e65 --- /dev/null +++ b/gulp/util/bundleLogger.js @@ -0,0 +1,25 @@ +/* bundleLogger + ------------ + Provides gulp style logs to the bundle method in browserify.js +*/ + +var gutil = require('gulp-util'); +var prettyHrtime = require('pretty-hrtime'); +var startTime; + +module.exports = { + start: function(filepath) { + startTime = process.hrtime(); + gutil.log('Bundling', gutil.colors.green(filepath) + '...'); + }, + + watch: function(bundleName) { + gutil.log('Watching files required by', gutil.colors.yellow(bundleName)); + }, + + end: function(filepath) { + var taskTime = process.hrtime(startTime); + var prettyTime = prettyHrtime(taskTime); + gutil.log('Bundled', gutil.colors.green(filepath), 'in', gutil.colors.magenta(prettyTime)); + } +}; diff --git a/gulp/util/handleErrors.js b/gulp/util/handleErrors.js new file mode 100644 index 000000000..a9f283490 --- /dev/null +++ b/gulp/util/handleErrors.js @@ -0,0 +1,15 @@ +var notify = require("gulp-notify"); + +module.exports = function() { + + var args = Array.prototype.slice.call(arguments); + + // Send error to notification center with gulp-notify + notify.onError({ + title: "Compile Error", + message: "<%= error %>" + }).apply(this, args); + + // Keep gulp from hanging on this task + this.emit('end'); +}; \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 000000000..59686d1fd --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,17 @@ +/* + gulpfile.js + =========== + Rather than manage one giant configuration file responsible + for creating multiple tasks, each task has been broken out into + its own file in gulp/tasks. Any files in that directory get + automatically required below. + + To add a new task, simply add a new task file that directory. + gulp/tasks/default.js specifies the default set of tasks to run + when you run `gulp`. +*/ + +var requireDir = require('require-dir'); + +// Require all tasks in gulp/tasks, including subfolders +requireDir('./gulp/tasks', { recurse: true }); diff --git a/index.js b/index.js new file mode 100644 index 000000000..59d7f26c4 --- /dev/null +++ b/index.js @@ -0,0 +1,7 @@ +// node entry point + +module.exports = function (window) { + require('./src/utils/window').init(window); + + return require('./src/index'); +}; diff --git a/interact.js b/interact.js deleted file mode 100644 index ffbd05d6c..000000000 --- a/interact.js +++ /dev/null @@ -1,5976 +0,0 @@ -/** - * interact.js v1.2.6 - * - * Copyright (c) 2012-2015 Taye Adeyemi - * Open source under the MIT License. - * https://raw.github.com/taye/interact.js/master/LICENSE - */ -(function (realWindow) { - 'use strict'; - - // return early if there's no window to work with (eg. Node.js) - if (!realWindow) { return; } - - var // get wrapped window if using Shadow DOM polyfill - window = (function () { - // create a TextNode - var el = realWindow.document.createTextNode(''); - - // check if it's wrapped by a polyfill - if (el.ownerDocument !== realWindow.document - && typeof realWindow.wrap === 'function' - && realWindow.wrap(el) === el) { - // return wrapped window - return realWindow.wrap(realWindow); - } - - // no Shadow DOM polyfil or native implementation - return realWindow; - }()), - - document = window.document, - DocumentFragment = window.DocumentFragment || blank, - SVGElement = window.SVGElement || blank, - SVGSVGElement = window.SVGSVGElement || blank, - SVGElementInstance = window.SVGElementInstance || blank, - HTMLElement = window.HTMLElement || window.Element, - - PointerEvent = (window.PointerEvent || window.MSPointerEvent), - pEventTypes, - - hypot = Math.hypot || function (x, y) { return Math.sqrt(x * x + y * y); }, - - tmpXY = {}, // reduce object creation in getXY() - - documents = [], // all documents being listened to - - interactables = [], // all set interactables - interactions = [], // all interactions - - dynamicDrop = false, - - // { - // type: { - // selectors: ['selector', ...], - // contexts : [document, ...], - // listeners: [[listener, useCapture], ...] - // } - // } - delegatedEvents = {}, - - defaultOptions = { - base: { - accept : null, - actionChecker : null, - styleCursor : true, - preventDefault: 'auto', - origin : { x: 0, y: 0 }, - deltaSource : 'page', - allowFrom : null, - ignoreFrom : null, - _context : document, - dropChecker : null - }, - - drag: { - enabled: false, - manualStart: true, - max: Infinity, - maxPerElement: 1, - - snap: null, - restrict: null, - inertia: null, - autoScroll: null, - - axis: 'xy' - }, - - drop: { - enabled: false, - accept: null, - overlap: 'pointer' - }, - - resize: { - enabled: false, - manualStart: false, - max: Infinity, - maxPerElement: 1, - - snap: null, - restrict: null, - inertia: null, - autoScroll: null, - - 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' - }, - - gesture: { - manualStart: false, - enabled: false, - max: Infinity, - maxPerElement: 1, - - restrict: null - }, - - perAction: { - manualStart: false, - max: Infinity, - maxPerElement: 1, - - snap: { - enabled : false, - endOnly : false, - range : Infinity, - targets : null, - offsets : null, - - relativePoints: null - }, - - restrict: { - enabled: false, - endOnly: false - }, - - autoScroll: { - enabled : false, - container : null, // the item that is scrolled (Window or HTMLElement) - margin : 60, - speed : 300 // the scroll speed in pixels per second - }, - - 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 - zeroResumeDelta : true, // if an action is resumed after launch, set dx/dy to 0 - smoothEndDuration: 300 // animate to snap/restrict endOnly if there's no inertia - } - }, - - _holdDuration: 600 - }, - - // Things related to autoScroll - autoScroll = { - interaction: null, - i: null, // the handle returned by window.setInterval - x: 0, y: 0, // Direction each pulse is to scroll in - - // scroll the window by the values in scroll.x/y - scroll: function () { - var options = autoScroll.interaction.target.options[autoScroll.interaction.prepared.name].autoScroll, - container = options.container || getWindow(autoScroll.interaction.element), - now = new Date().getTime(), - // change in time in seconds - dtx = (now - autoScroll.prevTimeX) / 1000, - dty = (now - autoScroll.prevTimeY) / 1000, - vx, vy, sx, sy; - - // displacement - if (options.velocity) { - vx = options.velocity.x; - vy = options.velocity.y; - } - else { - vx = vy = options.speed - } - - sx = vx * dtx; - sy = vy * dty; - - if (sx >= 1 || sy >= 1) { - if (isWindow(container)) { - container.scrollBy(autoScroll.x * sx, autoScroll.y * sy); - } - else if (container) { - container.scrollLeft += autoScroll.x * sx; - container.scrollTop += autoScroll.y * sy; - } - - if (sx >=1) autoScroll.prevTimeX = now; - if (sy >= 1) autoScroll.prevTimeY = now; - } - - if (autoScroll.isScrolling) { - cancelFrame(autoScroll.i); - autoScroll.i = reqFrame(autoScroll.scroll); - } - }, - - isScrolling: false, - prevTimeX: 0, - prevTimeY: 0, - - start: function (interaction) { - autoScroll.isScrolling = true; - cancelFrame(autoScroll.i); - - autoScroll.interaction = interaction; - autoScroll.prevTimeX = new Date().getTime(); - autoScroll.prevTimeY = new Date().getTime(); - autoScroll.i = reqFrame(autoScroll.scroll); - }, - - stop: function () { - autoScroll.isScrolling = false; - cancelFrame(autoScroll.i); - } - }, - - // Does the browser support touch input? - supportsTouch = (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch), - - // Does the browser support PointerEvents - supportsPointerEvent = !!PointerEvent, - - // Less Precision with touch input - margin = supportsTouch || supportsPointerEvent? 20: 10, - - pointerMoveTolerance = 1, - - // for ignoring browser's simulated mouse events - prevTouchTime = 0, - - // Allow this many interactions to happen simultaneously - maxInteractions = Infinity, - - // Check if is IE9 or older - actionCursors = (document.all && !window.atob) ? { - drag : 'move', - resizex : 'e-resize', - resizey : 's-resize', - resizexy: 'se-resize', - - resizetop : 'n-resize', - resizeleft : 'w-resize', - resizebottom : 's-resize', - resizeright : 'e-resize', - resizetopleft : 'se-resize', - resizebottomright: 'se-resize', - resizetopright : 'ne-resize', - resizebottomleft : 'ne-resize', - - gesture : '' - } : { - drag : 'move', - resizex : 'ew-resize', - resizey : 'ns-resize', - resizexy: 'nwse-resize', - - resizetop : 'ns-resize', - resizeleft : 'ew-resize', - resizebottom : 'ns-resize', - resizeright : 'ew-resize', - resizetopleft : 'nwse-resize', - resizebottomright: 'nwse-resize', - resizetopright : 'nesw-resize', - resizebottomleft : 'nesw-resize', - - gesture : '' - }, - - actionIsEnabled = { - drag : true, - resize : true, - gesture: true - }, - - // because Webkit and Opera still use 'mousewheel' event type - wheelEvent = 'onmousewheel' in document? 'mousewheel': 'wheel', - - eventTypes = [ - 'dragstart', - 'dragmove', - 'draginertiastart', - 'dragend', - 'dragenter', - 'dragleave', - 'dropactivate', - 'dropdeactivate', - 'dropmove', - 'drop', - 'resizestart', - 'resizemove', - 'resizeinertiastart', - 'resizeend', - 'gesturestart', - 'gesturemove', - 'gestureinertiastart', - 'gestureend', - - 'down', - 'move', - 'up', - 'cancel', - 'tap', - 'doubletap', - 'hold' - ], - - globalEvents = {}, - - // Opera Mobile must be handled differently - isOperaMobile = navigator.appName == 'Opera' && - supportsTouch && - navigator.userAgent.match('Presto'), - - // scrolling doesn't change the result of getClientRects on iOS 7 - isIOS7 = (/iP(hone|od|ad)/.test(navigator.platform) - && /OS 7[^\d]/.test(navigator.appVersion)), - - // prefix matchesSelector - prefixedMatchesSelector = 'matches' in Element.prototype? - 'matches': 'webkitMatchesSelector' in Element.prototype? - 'webkitMatchesSelector': 'mozMatchesSelector' in Element.prototype? - 'mozMatchesSelector': 'oMatchesSelector' in Element.prototype? - 'oMatchesSelector': 'msMatchesSelector', - - // will be polyfill function if browser is IE8 - ie8MatchesSelector, - - // native requestAnimationFrame or polyfill - reqFrame = realWindow.requestAnimationFrame, - cancelFrame = realWindow.cancelAnimationFrame, - - // Events wrapper - events = (function () { - var useAttachEvent = ('attachEvent' in window) && !('addEventListener' in window), - addEvent = useAttachEvent? 'attachEvent': 'addEventListener', - removeEvent = useAttachEvent? 'detachEvent': 'removeEventListener', - on = useAttachEvent? 'on': '', - - elements = [], - targets = [], - attachedListeners = []; - - function add (element, type, listener, useCapture) { - var elementIndex = indexOf(elements, element), - target = targets[elementIndex]; - - if (!target) { - target = { - events: {}, - typeCount: 0 - }; - - elementIndex = elements.push(element) - 1; - targets.push(target); - - attachedListeners.push((useAttachEvent ? { - supplied: [], - wrapped : [], - useCount: [] - } : null)); - } - - if (!target.events[type]) { - target.events[type] = []; - target.typeCount++; - } - - if (!contains(target.events[type], listener)) { - var ret; - - if (useAttachEvent) { - var listeners = attachedListeners[elementIndex], - listenerIndex = indexOf(listeners.supplied, listener); - - var wrapped = listeners.wrapped[listenerIndex] || function (event) { - if (!event.immediatePropagationStopped) { - event.target = event.srcElement; - event.currentTarget = element; - - event.preventDefault = event.preventDefault || preventDef; - event.stopPropagation = event.stopPropagation || stopProp; - event.stopImmediatePropagation = event.stopImmediatePropagation || stopImmProp; - - if (/mouse|click/.test(event.type)) { - event.pageX = event.clientX + getWindow(element).document.documentElement.scrollLeft; - event.pageY = event.clientY + getWindow(element).document.documentElement.scrollTop; - } - - listener(event); - } - }; - - ret = element[addEvent](on + type, wrapped, Boolean(useCapture)); - - if (listenerIndex === -1) { - listeners.supplied.push(listener); - listeners.wrapped.push(wrapped); - listeners.useCount.push(1); - } - else { - listeners.useCount[listenerIndex]++; - } - } - else { - ret = element[addEvent](type, listener, useCapture || false); - } - target.events[type].push(listener); - - return ret; - } - } - - function remove (element, type, listener, useCapture) { - var i, - elementIndex = indexOf(elements, element), - target = targets[elementIndex], - listeners, - listenerIndex, - wrapped = listener; - - if (!target || !target.events) { - return; - } - - if (useAttachEvent) { - listeners = attachedListeners[elementIndex]; - listenerIndex = indexOf(listeners.supplied, listener); - wrapped = listeners.wrapped[listenerIndex]; - } - - if (type === 'all') { - for (type in target.events) { - if (target.events.hasOwnProperty(type)) { - remove(element, type, 'all'); - } - } - return; - } - - if (target.events[type]) { - var len = target.events[type].length; - - if (listener === 'all') { - for (i = 0; i < len; i++) { - remove(element, type, target.events[type][i], Boolean(useCapture)); - } - return; - } else { - for (i = 0; i < len; i++) { - if (target.events[type][i] === listener) { - element[removeEvent](on + type, wrapped, useCapture || false); - target.events[type].splice(i, 1); - - if (useAttachEvent && listeners) { - listeners.useCount[listenerIndex]--; - if (listeners.useCount[listenerIndex] === 0) { - listeners.supplied.splice(listenerIndex, 1); - listeners.wrapped.splice(listenerIndex, 1); - listeners.useCount.splice(listenerIndex, 1); - } - } - - break; - } - } - } - - if (target.events[type] && target.events[type].length === 0) { - target.events[type] = null; - target.typeCount--; - } - } - - if (!target.typeCount) { - targets.splice(elementIndex, 1); - elements.splice(elementIndex, 1); - attachedListeners.splice(elementIndex, 1); - } - } - - function preventDef () { - this.returnValue = false; - } - - function stopProp () { - this.cancelBubble = true; - } - - function stopImmProp () { - this.cancelBubble = true; - this.immediatePropagationStopped = true; - } - - return { - add: add, - remove: remove, - useAttachEvent: useAttachEvent, - - _elements: elements, - _targets: targets, - _attachedListeners: attachedListeners - }; - }()); - - function blank () {} - - function isElement (o) { - if (!o || (typeof o !== 'object')) { return false; } - - var _window = getWindow(o) || window; - - return (/object|function/.test(typeof _window.Element) - ? o instanceof _window.Element //DOM2 - : o.nodeType === 1 && typeof o.nodeName === "string"); - } - function isWindow (thing) { return thing === window || !!(thing && thing.Window) && (thing instanceof thing.Window); } - function isDocFrag (thing) { return !!thing && thing instanceof DocumentFragment; } - function isArray (thing) { - return isObject(thing) - && (typeof thing.length !== undefined) - && isFunction(thing.splice); - } - function isObject (thing) { return !!thing && (typeof thing === 'object'); } - function isFunction (thing) { return typeof thing === 'function'; } - function isNumber (thing) { return typeof thing === 'number' ; } - function isBool (thing) { return typeof thing === 'boolean' ; } - function isString (thing) { return typeof thing === 'string' ; } - - function trySelector (value) { - if (!isString(value)) { return false; } - - // an exception will be raised if it is invalid - document.querySelector(value); - return true; - } - - function extend (dest, source) { - for (var prop in source) { - dest[prop] = source[prop]; - } - return dest; - } - - var prefixedPropREs = { - webkit: /(Movement[XY]|Radius[XY]|RotationAngle|Force)$/ - }; - - function pointerExtend (dest, source) { - for (var prop in source) { - var deprecated = false; - - // skip deprecated prefixed properties - for (var vendor in prefixedPropREs) { - if (prop.indexOf(vendor) === 0 && prefixedPropREs[vendor].test(prop)) { - deprecated = true; - break; - } - } - - if (!deprecated) { - dest[prop] = source[prop]; - } - } - return dest; - } - - function copyCoords (dest, src) { - dest.page = dest.page || {}; - dest.page.x = src.page.x; - dest.page.y = src.page.y; - - dest.client = dest.client || {}; - dest.client.x = src.client.x; - dest.client.y = src.client.y; - - dest.timeStamp = src.timeStamp; - } - - function setEventXY (targetObj, pointers, interaction) { - var pointer = (pointers.length > 1 - ? pointerAverage(pointers) - : pointers[0]); - - getPageXY(pointer, tmpXY, interaction); - targetObj.page.x = tmpXY.x; - targetObj.page.y = tmpXY.y; - - getClientXY(pointer, tmpXY, interaction); - targetObj.client.x = tmpXY.x; - targetObj.client.y = tmpXY.y; - - targetObj.timeStamp = new Date().getTime(); - } - - function setEventDeltas (targetObj, prev, cur) { - 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 = new Date().getTime() - prev.timeStamp; - - // set pointer velocity - var dt = Math.max(targetObj.timeStamp / 1000, 0.001); - targetObj.page.speed = hypot(targetObj.page.x, targetObj.page.y) / dt; - targetObj.page.vx = targetObj.page.x / dt; - targetObj.page.vy = targetObj.page.y / dt; - - targetObj.client.speed = hypot(targetObj.client.x, targetObj.page.y) / dt; - targetObj.client.vx = targetObj.client.x / dt; - targetObj.client.vy = targetObj.client.y / dt; - } - - function isNativePointer (pointer) { - return (pointer instanceof window.Event - || (supportsTouch && window.Touch && pointer instanceof window.Touch)); - } - - // Get specified X/Y coords for mouse or event.touches[0] - function getXY (type, pointer, xy) { - xy = xy || {}; - type = type || 'page'; - - xy.x = pointer[type + 'X']; - xy.y = pointer[type + 'Y']; - - return xy; - } - - function getPageXY (pointer, page) { - page = page || {}; - - // Opera Mobile handles the viewport and scrolling oddly - if (isOperaMobile && isNativePointer(pointer)) { - getXY('screen', pointer, page); - - page.x += window.scrollX; - page.y += window.scrollY; - } - else { - getXY('page', pointer, page); - } - - return page; - } - - function getClientXY (pointer, client) { - client = client || {}; - - if (isOperaMobile && isNativePointer(pointer)) { - // Opera Mobile handles the viewport and scrolling oddly - getXY('screen', pointer, client); - } - else { - getXY('client', pointer, client); - } - - return client; - } - - function getScrollXY (win) { - win = win || window; - return { - x: win.scrollX || win.document.documentElement.scrollLeft, - y: win.scrollY || win.document.documentElement.scrollTop - }; - } - - function getPointerId (pointer) { - return isNumber(pointer.pointerId)? pointer.pointerId : pointer.identifier; - } - - function getActualElement (element) { - return (element instanceof SVGElementInstance - ? element.correspondingUseElement - : element); - } - - function getWindow (node) { - if (isWindow(node)) { - return node; - } - - var rootNode = (node.ownerDocument || node); - - return rootNode.defaultView || rootNode.parentWindow || window; - } - - function getElementClientRect (element) { - var clientRect = (element instanceof 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 - }; - } - - function getElementRect (element) { - var clientRect = getElementClientRect(element); - - if (!isIOS7 && clientRect) { - var scroll = getScrollXY(getWindow(element)); - - clientRect.left += scroll.x; - clientRect.right += scroll.x; - clientRect.top += scroll.y; - clientRect.bottom += scroll.y; - } - - return clientRect; - } - - function getTouchPair (event) { - var touches = []; - - // array of touches is supplied - if (isArray(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; - } - - function pointerAverage (pointers) { - var average = { - pageX : 0, - pageY : 0, - clientX: 0, - clientY: 0, - screenX: 0, - screenY: 0 - }; - var prop; - - for (var i = 0; i < pointers.length; i++) { - for (prop in average) { - average[prop] += pointers[i][prop]; - } - } - for (prop in average) { - average[prop] /= pointers.length; - } - - return average; - } - - function touchBBox (event) { - if (!event.length && !(event.touches && event.touches.length > 1)) { - return; - } - - var touches = getTouchPair(event), - minX = Math.min(touches[0].pageX, touches[1].pageX), - minY = Math.min(touches[0].pageY, touches[1].pageY), - maxX = Math.max(touches[0].pageX, touches[1].pageX), - maxY = Math.max(touches[0].pageY, touches[1].pageY); - - return { - x: minX, - y: minY, - left: minX, - top: minY, - width: maxX - minX, - height: maxY - minY - }; - } - - function touchDistance (event, deltaSource) { - deltaSource = deltaSource || defaultOptions.deltaSource; - - var sourceX = deltaSource + 'X', - sourceY = deltaSource + 'Y', - touches = getTouchPair(event); - - - var dx = touches[0][sourceX] - touches[1][sourceX], - dy = touches[0][sourceY] - touches[1][sourceY]; - - return hypot(dx, dy); - } - - function touchAngle (event, prevAngle, deltaSource) { - deltaSource = deltaSource || defaultOptions.deltaSource; - - var sourceX = deltaSource + 'X', - sourceY = deltaSource + 'Y', - touches = getTouchPair(event), - dx = touches[0][sourceX] - touches[1][sourceX], - dy = touches[0][sourceY] - touches[1][sourceY], - angle = 180 * Math.atan(dy / dx) / Math.PI; - - if (isNumber(prevAngle)) { - var dr = angle - prevAngle, - drClamped = dr % 360; - - if (drClamped > 315) { - angle -= 360 + (angle / 360)|0 * 360; - } - else if (drClamped > 135) { - angle -= 180 + (angle / 360)|0 * 360; - } - else if (drClamped < -315) { - angle += 360 + (angle / 360)|0 * 360; - } - else if (drClamped < -135) { - angle += 180 + (angle / 360)|0 * 360; - } - } - - return angle; - } - - function getOriginXY (interactable, element) { - var origin = interactable - ? interactable.options.origin - : defaultOptions.origin; - - if (origin === 'parent') { - origin = parentElement(element); - } - else if (origin === 'self') { - origin = interactable.getRect(element); - } - else if (trySelector(origin)) { - origin = closest(element, origin) || { x: 0, y: 0 }; - } - - if (isFunction(origin)) { - origin = origin(interactable && element); - } - - if (isElement(origin)) { - origin = getElementRect(origin); - } - - origin.x = ('x' in origin)? origin.x : origin.left; - origin.y = ('y' in origin)? origin.y : origin.top; - - return origin; - } - - // http://stackoverflow.com/a/5634528/2280888 - function _getQBezierValue(t, p1, p2, p3) { - var iT = 1 - t; - return iT * iT * p1 + 2 * iT * t * p2 + t * t * p3; - } - - function getQuadraticCurvePoint(startX, startY, cpX, cpY, endX, endY, position) { - return { - x: _getQBezierValue(position, startX, cpX, endX), - y: _getQBezierValue(position, startY, cpY, endY) - }; - } - - // http://gizma.com/easing/ - function easeOutQuad (t, b, c, d) { - t /= d; - return -c * t*(t-2) + b; - } - - function nodeContains (parent, child) { - while (child) { - if (child === parent) { - return true; - } - - child = child.parentNode; - } - - return false; - } - - function closest (child, selector) { - var parent = parentElement(child); - - while (isElement(parent)) { - if (matchesSelector(parent, selector)) { return parent; } - - parent = parentElement(parent); - } - - return null; - } - - function parentElement (node) { - var parent = node.parentNode; - - if (isDocFrag(parent)) { - // skip past #shado-root fragments - while ((parent = parent.host) && isDocFrag(parent)) {} - - return parent; - } - - return parent; - } - - function inContext (interactable, element) { - return interactable._context === element.ownerDocument - || nodeContains(interactable._context, element); - } - - function testIgnore (interactable, interactableElement, element) { - var ignoreFrom = interactable.options.ignoreFrom; - - if (!ignoreFrom || !isElement(element)) { return false; } - - if (isString(ignoreFrom)) { - return matchesUpTo(element, ignoreFrom, interactableElement); - } - else if (isElement(ignoreFrom)) { - return nodeContains(ignoreFrom, element); - } - - return false; - } - - function testAllow (interactable, interactableElement, element) { - var allowFrom = interactable.options.allowFrom; - - if (!allowFrom) { return true; } - - if (!isElement(element)) { return false; } - - if (isString(allowFrom)) { - return matchesUpTo(element, allowFrom, interactableElement); - } - else if (isElement(allowFrom)) { - return nodeContains(allowFrom, element); - } - - return false; - } - - function checkAxis (axis, interactable) { - if (!interactable) { return false; } - - var thisAxis = interactable.options.drag.axis; - - return (axis === 'xy' || thisAxis === 'xy' || thisAxis === axis); - } - - function checkSnap (interactable, action) { - var options = interactable.options; - - if (/^resize/.test(action)) { - action = 'resize'; - } - - return options[action].snap && options[action].snap.enabled; - } - - function checkRestrict (interactable, action) { - var options = interactable.options; - - if (/^resize/.test(action)) { - action = 'resize'; - } - - return options[action].restrict && options[action].restrict.enabled; - } - - function checkAutoScroll (interactable, action) { - var options = interactable.options; - - if (/^resize/.test(action)) { - action = 'resize'; - } - - return options[action].autoScroll && options[action].autoScroll.enabled; - } - - function withinInteractionLimit (interactable, element, action) { - var options = interactable.options, - maxActions = options[action.name].max, - maxPerElement = options[action.name].maxPerElement, - activeInteractions = 0, - targetCount = 0, - targetElementCount = 0; - - for (var i = 0, len = interactions.length; i < len; i++) { - var interaction = interactions[i], - otherAction = interaction.prepared.name, - active = interaction.interacting(); - - if (!active) { continue; } - - activeInteractions++; - - if (activeInteractions >= maxInteractions) { - return false; - } - - if (interaction.target !== interactable) { continue; } - - targetCount += (otherAction === action.name)|0; - - if (targetCount >= maxActions) { - return false; - } - - if (interaction.element === element) { - targetElementCount++; - - if (otherAction !== action.name || targetElementCount >= maxPerElement) { - return false; - } - } - } - - return maxInteractions > 0; - } - - // Test for the element that's "above" all other qualifiers - function indexOfDeepestElement (elements) { - var dropzone, - deepestZone = elements[0], - index = deepestZone? 0: -1, - parent, - deepestZoneParents = [], - dropzoneParents = [], - child, - i, - n; - - for (i = 1; i < elements.length; i++) { - dropzone = elements[i]; - - // an element might belong to multiple selector dropzones - if (!dropzone || dropzone === deepestZone) { - continue; - } - - if (!deepestZone) { - deepestZone = dropzone; - index = i; - continue; - } - - // check if the deepest or current are document.documentElement or document.rootElement - // - if the current dropzone is, do nothing and continue - if (dropzone.parentNode === dropzone.ownerDocument) { - continue; - } - // - if deepest is, update with the current dropzone and continue to next - else if (deepestZone.parentNode === dropzone.ownerDocument) { - deepestZone = dropzone; - index = i; - continue; - } - - if (!deepestZoneParents.length) { - parent = deepestZone; - while (parent.parentNode && parent.parentNode !== parent.ownerDocument) { - deepestZoneParents.unshift(parent); - parent = parent.parentNode; - } - } - - // if this element is an svg element and the current deepest is - // an HTMLElement - if (deepestZone instanceof HTMLElement - && dropzone instanceof SVGElement - && !(dropzone instanceof SVGSVGElement)) { - - if (dropzone === deepestZone.parentNode) { - continue; - } - - parent = dropzone.ownerSVGElement; - } - else { - parent = dropzone; - } - - dropzoneParents = []; - - while (parent.parentNode !== parent.ownerDocument) { - dropzoneParents.unshift(parent); - parent = parent.parentNode; - } - - n = 0; - - // get (position of last common ancestor) + 1 - while (dropzoneParents[n] && dropzoneParents[n] === deepestZoneParents[n]) { - n++; - } - - var parents = [ - dropzoneParents[n - 1], - dropzoneParents[n], - deepestZoneParents[n] - ]; - - child = parents[0].lastChild; - - while (child) { - if (child === parents[1]) { - deepestZone = dropzone; - index = i; - deepestZoneParents = []; - - break; - } - else if (child === parents[2]) { - break; - } - - child = child.previousSibling; - } - } - - return index; - } - - function Interaction () { - this.target = null; // current interactable being interacted with - this.element = null; // the target element of the interactable - this.dropTarget = null; // the dropzone a drag target might be dropped into - this.dropElement = null; // the element at the time of checking - this.prevDropTarget = null; // the dropzone that was recently dragged away from - this.prevDropElement = null; // the element at the time of checking - - this.prepared = { // action that's ready to be fired on next move event - name : null, - axis : null, - edges: null - }; - - this.matches = []; // all selectors that are matched by target element - this.matchElements = []; // corresponding elements - - this.inertiaStatus = { - active : false, - smoothEnd : false, - ending : false, - - startEvent: null, - upCoords: {}, - - xe: 0, ye: 0, - sx: 0, sy: 0, - - t0: 0, - vx0: 0, vys: 0, - duration: 0, - - resumeDx: 0, - resumeDy: 0, - - lambda_v0: 0, - one_ve_v0: 0, - i : null - }; - - if (isFunction(Function.prototype.bind)) { - this.boundInertiaFrame = this.inertiaFrame.bind(this); - this.boundSmoothEndFrame = this.smoothEndFrame.bind(this); - } - else { - var that = this; - - this.boundInertiaFrame = function () { return that.inertiaFrame(); }; - this.boundSmoothEndFrame = function () { return that.smoothEndFrame(); }; - } - - this.activeDrops = { - dropzones: [], // the dropzones that are mentioned below - elements : [], // elements of dropzones that accept the target draggable - rects : [] // the rects of the elements mentioned above - }; - - // keep track of added pointers - this.pointers = []; - this.pointerIds = []; - this.downTargets = []; - this.downTimes = []; - this.holdTimers = []; - - // Previous native pointer move event coordinates - this.prevCoords = { - page : { x: 0, y: 0 }, - client : { x: 0, y: 0 }, - timeStamp: 0 - }; - // current native pointer move event coordinates - this.curCoords = { - page : { x: 0, y: 0 }, - client : { x: 0, y: 0 }, - timeStamp: 0 - }; - - // Starting InteractEvent pointer coordinates - this.startCoords = { - page : { x: 0, y: 0 }, - client : { x: 0, y: 0 }, - timeStamp: 0 - }; - - // Change in coordinates and time of the pointer - this.pointerDelta = { - page : { x: 0, y: 0, vx: 0, vy: 0, speed: 0 }, - client : { x: 0, y: 0, vx: 0, vy: 0, speed: 0 }, - timeStamp: 0 - }; - - this.downEvent = null; // pointerdown/mousedown/touchstart event - this.downPointer = {}; - - this._eventTarget = null; - this._curEventTarget = null; - - this.prevEvent = null; // previous action event - this.tapTime = 0; // time of the most recent tap event - this.prevTap = null; - - this.startOffset = { left: 0, right: 0, top: 0, bottom: 0 }; - this.restrictOffset = { left: 0, right: 0, top: 0, bottom: 0 }; - this.snapOffsets = []; - - this.gesture = { - start: { x: 0, y: 0 }, - - startDistance: 0, // distance between two touches of touchStart - prevDistance : 0, - distance : 0, - - scale: 1, // gesture.distance / gesture.startDistance - - startAngle: 0, // angle of line joining two touches - prevAngle : 0 // angle of the previous gesture event - }; - - this.snapStatus = { - x : 0, y : 0, - dx : 0, dy : 0, - realX : 0, realY : 0, - snappedX: 0, snappedY: 0, - targets : [], - locked : false, - changed : false - }; - - this.restrictStatus = { - dx : 0, dy : 0, - restrictedX: 0, restrictedY: 0, - snap : null, - restricted : false, - changed : false - }; - - this.restrictStatus.snap = this.snapStatus; - - this.pointerIsDown = false; - this.pointerWasMoved = false; - this.gesturing = false; - this.dragging = false; - this.resizing = false; - this.resizeAxes = 'xy'; - - this.mouse = false; - - interactions.push(this); - } - - Interaction.prototype = { - getPageXY : function (pointer, xy) { return getPageXY(pointer, xy, this); }, - getClientXY: function (pointer, xy) { return getClientXY(pointer, xy, this); }, - setEventXY : function (target, ptr) { return setEventXY(target, ptr, this); }, - - pointerOver: function (pointer, event, eventTarget) { - if (this.prepared.name || !this.mouse) { return; } - - var curMatches = [], - curMatchElements = [], - prevTargetElement = this.element; - - this.addPointer(pointer); - - if (this.target - && (testIgnore(this.target, this.element, eventTarget) - || !testAllow(this.target, this.element, eventTarget))) { - // if the eventTarget should be ignored or shouldn't be allowed - // clear the previous target - this.target = null; - this.element = null; - this.matches = []; - this.matchElements = []; - } - - var elementInteractable = interactables.get(eventTarget), - elementAction = (elementInteractable - && !testIgnore(elementInteractable, eventTarget, eventTarget) - && testAllow(elementInteractable, eventTarget, eventTarget) - && validateAction( - elementInteractable.getAction(pointer, event, this, eventTarget), - elementInteractable)); - - if (elementAction && !withinInteractionLimit(elementInteractable, eventTarget, elementAction)) { - elementAction = null; - } - - function pushCurMatches (interactable, selector) { - if (interactable - && inContext(interactable, eventTarget) - && !testIgnore(interactable, eventTarget, eventTarget) - && testAllow(interactable, eventTarget, eventTarget) - && matchesSelector(eventTarget, selector)) { - - curMatches.push(interactable); - curMatchElements.push(eventTarget); - } - } - - if (elementAction) { - this.target = elementInteractable; - this.element = eventTarget; - this.matches = []; - this.matchElements = []; - } - else { - interactables.forEachSelector(pushCurMatches); - - if (this.validateSelector(pointer, event, curMatches, curMatchElements)) { - this.matches = curMatches; - this.matchElements = curMatchElements; - - this.pointerHover(pointer, event, this.matches, this.matchElements); - events.add(eventTarget, - PointerEvent? pEventTypes.move : 'mousemove', - listeners.pointerHover); - } - else if (this.target) { - if (nodeContains(prevTargetElement, eventTarget)) { - this.pointerHover(pointer, event, this.matches, this.matchElements); - events.add(this.element, - PointerEvent? pEventTypes.move : 'mousemove', - listeners.pointerHover); - } - else { - this.target = null; - this.element = null; - this.matches = []; - this.matchElements = []; - } - } - } - }, - - // Check what action would be performed on pointerMove target if a mouse - // button were pressed and change the cursor accordingly - pointerHover: function (pointer, event, eventTarget, curEventTarget, matches, matchElements) { - var target = this.target; - - if (!this.prepared.name && this.mouse) { - - var action; - - // update pointer coords for defaultActionChecker to use - this.setEventXY(this.curCoords, [pointer]); - - if (matches) { - action = this.validateSelector(pointer, event, matches, matchElements); - } - else if (target) { - action = validateAction(target.getAction(this.pointers[0], event, this, this.element), this.target); - } - - if (target && target.options.styleCursor) { - if (action) { - target._doc.documentElement.style.cursor = getActionCursor(action); - } - else { - target._doc.documentElement.style.cursor = ''; - } - } - } - else if (this.prepared.name) { - this.checkAndPreventDefault(event, target, this.element); - } - }, - - pointerOut: function (pointer, event, eventTarget) { - if (this.prepared.name) { return; } - - // Remove temporary event listeners for selector Interactables - if (!interactables.get(eventTarget)) { - events.remove(eventTarget, - PointerEvent? pEventTypes.move : 'mousemove', - listeners.pointerHover); - } - - if (this.target && this.target.options.styleCursor && !this.interacting()) { - this.target._doc.documentElement.style.cursor = ''; - } - }, - - selectorDown: function (pointer, event, eventTarget, curEventTarget) { - var that = this, - // copy event to be used in timeout for IE8 - eventCopy = events.useAttachEvent? extend({}, event) : event, - element = eventTarget, - pointerIndex = this.addPointer(pointer), - action; - - this.holdTimers[pointerIndex] = setTimeout(function () { - that.pointerHold(events.useAttachEvent? eventCopy : pointer, eventCopy, eventTarget, curEventTarget); - }, defaultOptions._holdDuration); - - this.pointerIsDown = true; - - // Check if the down event hits the current inertia target - if (this.inertiaStatus.active && this.target.selector) { - // climb up the DOM tree from the event target - while (isElement(element)) { - - // if this element is the current inertia target element - if (element === this.element - // and the prospective action is the same as the ongoing one - && validateAction(this.target.getAction(pointer, event, this, this.element), this.target).name === this.prepared.name) { - - // stop inertia so that the next move will be a normal one - cancelFrame(this.inertiaStatus.i); - this.inertiaStatus.active = false; - - this.collectEventTargets(pointer, event, eventTarget, 'down'); - return; - } - element = parentElement(element); - } - } - - // do nothing if interacting - if (this.interacting()) { - this.collectEventTargets(pointer, event, eventTarget, 'down'); - return; - } - - function pushMatches (interactable, selector, context) { - var elements = ie8MatchesSelector - ? context.querySelectorAll(selector) - : undefined; - - if (inContext(interactable, element) - && !testIgnore(interactable, element, eventTarget) - && testAllow(interactable, element, eventTarget) - && matchesSelector(element, selector, elements)) { - - that.matches.push(interactable); - that.matchElements.push(element); - } - } - - // update pointer coords for defaultActionChecker to use - this.setEventXY(this.curCoords, [pointer]); - this.downEvent = event; - - while (isElement(element) && !action) { - this.matches = []; - this.matchElements = []; - - interactables.forEachSelector(pushMatches); - - action = this.validateSelector(pointer, event, this.matches, this.matchElements); - element = parentElement(element); - } - - if (action) { - this.prepared.name = action.name; - this.prepared.axis = action.axis; - this.prepared.edges = action.edges; - - this.collectEventTargets(pointer, event, eventTarget, 'down'); - - return this.pointerDown(pointer, event, eventTarget, curEventTarget, action); - } - else { - // do these now since pointerDown isn't being called from here - this.downTimes[pointerIndex] = new Date().getTime(); - this.downTargets[pointerIndex] = eventTarget; - pointerExtend(this.downPointer, pointer); - - copyCoords(this.prevCoords, this.curCoords); - this.pointerWasMoved = false; - } - - this.collectEventTargets(pointer, event, eventTarget, 'down'); - }, - - // Determine action to be performed on next pointerMove and add appropriate - // style and event Listeners - pointerDown: function (pointer, event, eventTarget, curEventTarget, forceAction) { - if (!forceAction && !this.inertiaStatus.active && this.pointerWasMoved && this.prepared.name) { - this.checkAndPreventDefault(event, this.target, this.element); - - return; - } - - this.pointerIsDown = true; - this.downEvent = event; - - var pointerIndex = this.addPointer(pointer), - action; - - // If it is the second touch of a multi-touch gesture, keep the - // target the same and get a new action if a target was set by the - // first touch - if (this.pointerIds.length > 1 && this.target._element === this.element) { - var newAction = validateAction(forceAction || this.target.getAction(pointer, event, this, this.element), this.target); - - if (withinInteractionLimit(this.target, this.element, newAction)) { - action = newAction; - } - - this.prepared.name = null; - } - // Otherwise, set the target if there is no action prepared - else if (!this.prepared.name) { - var interactable = interactables.get(curEventTarget); - - if (interactable - && !testIgnore(interactable, curEventTarget, eventTarget) - && testAllow(interactable, curEventTarget, eventTarget) - && (action = validateAction(forceAction || interactable.getAction(pointer, event, this, curEventTarget), interactable, eventTarget)) - && withinInteractionLimit(interactable, curEventTarget, action)) { - this.target = interactable; - this.element = curEventTarget; - } - } - - var target = this.target, - options = target && target.options; - - if (target && (forceAction || !this.prepared.name)) { - action = action || validateAction(forceAction || target.getAction(pointer, event, this, curEventTarget), target, this.element); - - this.setEventXY(this.startCoords, this.pointers); - - if (!action) { return; } - - if (options.styleCursor) { - target._doc.documentElement.style.cursor = getActionCursor(action); - } - - this.resizeAxes = action.name === 'resize'? action.axis : null; - - if (action === 'gesture' && this.pointerIds.length < 2) { - action = null; - } - - this.prepared.name = action.name; - this.prepared.axis = action.axis; - this.prepared.edges = action.edges; - - this.snapStatus.snappedX = this.snapStatus.snappedY = - this.restrictStatus.restrictedX = this.restrictStatus.restrictedY = NaN; - - this.downTimes[pointerIndex] = new Date().getTime(); - this.downTargets[pointerIndex] = eventTarget; - pointerExtend(this.downPointer, pointer); - - copyCoords(this.prevCoords, this.startCoords); - this.pointerWasMoved = false; - - this.checkAndPreventDefault(event, target, this.element); - } - // if inertia is active try to resume action - else if (this.inertiaStatus.active - && curEventTarget === this.element - && validateAction(target.getAction(pointer, event, this, this.element), target).name === this.prepared.name) { - - cancelFrame(this.inertiaStatus.i); - this.inertiaStatus.active = false; - - this.checkAndPreventDefault(event, target, this.element); - } - }, - - setModifications: function (coords, preEnd) { - var target = this.target, - shouldMove = true, - shouldSnap = checkSnap(target, this.prepared.name) && (!target.options[this.prepared.name].snap.endOnly || preEnd), - shouldRestrict = checkRestrict(target, this.prepared.name) && (!target.options[this.prepared.name].restrict.endOnly || preEnd); - - if (shouldSnap ) { this.setSnapping (coords); } else { this.snapStatus .locked = false; } - if (shouldRestrict) { this.setRestriction(coords); } else { this.restrictStatus.restricted = false; } - - if (shouldSnap && this.snapStatus.locked && !this.snapStatus.changed) { - shouldMove = shouldRestrict && this.restrictStatus.restricted && this.restrictStatus.changed; - } - else if (shouldRestrict && this.restrictStatus.restricted && !this.restrictStatus.changed) { - shouldMove = false; - } - - return shouldMove; - }, - - setStartOffsets: function (action, interactable, element) { - var rect = interactable.getRect(element), - origin = getOriginXY(interactable, element), - snap = interactable.options[this.prepared.name].snap, - restrict = interactable.options[this.prepared.name].restrict, - width, height; - - if (rect) { - this.startOffset.left = this.startCoords.page.x - rect.left; - this.startOffset.top = this.startCoords.page.y - rect.top; - - this.startOffset.right = rect.right - this.startCoords.page.x; - this.startOffset.bottom = rect.bottom - this.startCoords.page.y; - - if ('width' in rect) { width = rect.width; } - else { width = rect.right - rect.left; } - if ('height' in rect) { height = rect.height; } - else { height = rect.bottom - rect.top; } - } - else { - this.startOffset.left = this.startOffset.top = this.startOffset.right = this.startOffset.bottom = 0; - } - - this.snapOffsets.splice(0); - - var snapOffset = snap && snap.offset === 'startCoords' - ? { - x: this.startCoords.page.x - origin.x, - y: this.startCoords.page.y - origin.y - } - : snap && snap.offset || { x: 0, y: 0 }; - - if (rect && snap && snap.relativePoints && snap.relativePoints.length) { - for (var i = 0; i < snap.relativePoints.length; i++) { - this.snapOffsets.push({ - x: this.startOffset.left - (width * snap.relativePoints[i].x) + snapOffset.x, - y: this.startOffset.top - (height * snap.relativePoints[i].y) + snapOffset.y - }); - } - } - else { - this.snapOffsets.push(snapOffset); - } - - if (rect && restrict.elementRect) { - this.restrictOffset.left = this.startOffset.left - (width * restrict.elementRect.left); - this.restrictOffset.top = this.startOffset.top - (height * restrict.elementRect.top); - - this.restrictOffset.right = this.startOffset.right - (width * (1 - restrict.elementRect.right)); - this.restrictOffset.bottom = this.startOffset.bottom - (height * (1 - restrict.elementRect.bottom)); - } - else { - this.restrictOffset.left = this.restrictOffset.top = this.restrictOffset.right = this.restrictOffset.bottom = 0; - } - }, - - /*\ - * Interaction.start - [ method ] - * - * 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) - * - - action (object) The action to be performed - drag, resize, etc. - - interactable (Interactable) The Interactable to target - - element (Element) The DOM Element to target - = (object) interact - ** - | 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: function (action, interactable, element) { - if (this.interacting() - || !this.pointerIsDown - || this.pointerIds.length < (action.name === 'gesture'? 2 : 1)) { - return; - } - - // if this interaction had been removed after stopping - // add it back - if (indexOf(interactions, this) === -1) { - interactions.push(this); - } - - // set the startCoords if there was no prepared action - if (!this.prepared.name) { - this.setEventXY(this.startCoords); - } - - this.prepared.name = action.name; - this.prepared.axis = action.axis; - this.prepared.edges = action.edges; - this.target = interactable; - this.element = element; - - this.setStartOffsets(action.name, interactable, element); - this.setModifications(this.startCoords.page); - - this.prevEvent = this[this.prepared.name + 'Start'](this.downEvent); - }, - - pointerMove: function (pointer, event, eventTarget, curEventTarget, preEnd) { - if (this.inertiaStatus.active) { - var pageUp = this.inertiaStatus.upCoords.page; - var clientUp = this.inertiaStatus.upCoords.client; - - var inertiaPosition = { - pageX : pageUp.x + this.inertiaStatus.sx, - pageY : pageUp.y + this.inertiaStatus.sy, - clientX: clientUp.x + this.inertiaStatus.sx, - clientY: clientUp.y + this.inertiaStatus.sy - }; - - this.setEventXY(this.curCoords, [inertiaPosition]); - } - else { - this.recordPointer(pointer); - this.setEventXY(this.curCoords, this.pointers); - } - - var duplicateMove = (this.curCoords.page.x === this.prevCoords.page.x - && this.curCoords.page.y === this.prevCoords.page.y - && this.curCoords.client.x === this.prevCoords.client.x - && this.curCoords.client.y === this.prevCoords.client.y); - - var dx, dy, - pointerIndex = this.mouse? 0 : indexOf(this.pointerIds, getPointerId(pointer)); - - // register movement greater than pointerMoveTolerance - if (this.pointerIsDown && !this.pointerWasMoved) { - dx = this.curCoords.client.x - this.startCoords.client.x; - dy = this.curCoords.client.y - this.startCoords.client.y; - - this.pointerWasMoved = hypot(dx, dy) > pointerMoveTolerance; - } - - if (!duplicateMove && (!this.pointerIsDown || this.pointerWasMoved)) { - if (this.pointerIsDown) { - clearTimeout(this.holdTimers[pointerIndex]); - } - - this.collectEventTargets(pointer, event, eventTarget, 'move'); - } - - if (!this.pointerIsDown) { return; } - - if (duplicateMove && this.pointerWasMoved && !preEnd) { - this.checkAndPreventDefault(event, this.target, this.element); - return; - } - - // set pointer coordinate, time changes and speeds - setEventDeltas(this.pointerDelta, this.prevCoords, this.curCoords); - - if (!this.prepared.name) { return; } - - if (this.pointerWasMoved - // ignore movement while inertia is active - && (!this.inertiaStatus.active || (pointer instanceof InteractEvent && /inertiastart/.test(pointer.type)))) { - - // if just starting an action, calculate the pointer speed now - if (!this.interacting()) { - setEventDeltas(this.pointerDelta, this.prevCoords, this.curCoords); - - // check if a drag is in the correct axis - if (this.prepared.name === 'drag') { - var absX = Math.abs(dx), - absY = Math.abs(dy), - targetAxis = this.target.options.drag.axis, - axis = (absX > absY ? 'x' : absX < absY ? 'y' : 'xy'); - - // if the movement isn't in the axis of the interactable - if (axis !== 'xy' && targetAxis !== 'xy' && targetAxis !== axis) { - // cancel the prepared action - this.prepared.name = null; - - // then try to get a drag from another ineractable - - var element = eventTarget; - - // check element interactables - while (isElement(element)) { - var elementInteractable = interactables.get(element); - - if (elementInteractable - && elementInteractable !== this.target - && !elementInteractable.options.drag.manualStart - && elementInteractable.getAction(this.downPointer, this.downEvent, this, element).name === 'drag' - && checkAxis(axis, elementInteractable)) { - - this.prepared.name = 'drag'; - this.target = elementInteractable; - this.element = element; - break; - } - - element = parentElement(element); - } - - // if there's no drag from element interactables, - // check the selector interactables - if (!this.prepared.name) { - var thisInteraction = this; - - var getDraggable = function (interactable, selector, context) { - var elements = ie8MatchesSelector - ? context.querySelectorAll(selector) - : undefined; - - if (interactable === thisInteraction.target) { return; } - - if (inContext(interactable, eventTarget) - && !interactable.options.drag.manualStart - && !testIgnore(interactable, element, eventTarget) - && testAllow(interactable, element, eventTarget) - && matchesSelector(element, selector, elements) - && interactable.getAction(thisInteraction.downPointer, thisInteraction.downEvent, thisInteraction, element).name === 'drag' - && checkAxis(axis, interactable) - && withinInteractionLimit(interactable, element, 'drag')) { - - return interactable; - } - }; - - element = eventTarget; - - while (isElement(element)) { - var selectorInteractable = interactables.forEachSelector(getDraggable); - - if (selectorInteractable) { - this.prepared.name = 'drag'; - this.target = selectorInteractable; - this.element = element; - break; - } - - element = parentElement(element); - } - } - } - } - } - - var starting = !!this.prepared.name && !this.interacting(); - - if (starting - && (this.target.options[this.prepared.name].manualStart - || !withinInteractionLimit(this.target, this.element, this.prepared))) { - this.stop(event); - return; - } - - if (this.prepared.name && this.target) { - if (starting) { - this.start(this.prepared, this.target, this.element); - } - - var shouldMove = this.setModifications(this.curCoords.page, preEnd); - - // move if snapping or restriction doesn't prevent it - if (shouldMove || starting) { - this.prevEvent = this[this.prepared.name + 'Move'](event); - } - - this.checkAndPreventDefault(event, this.target, this.element); - } - } - - copyCoords(this.prevCoords, this.curCoords); - - if (this.dragging || this.resizing) { - this.autoScrollMove(pointer); - } - }, - - dragStart: function (event) { - var dragEvent = new InteractEvent(this, event, 'drag', 'start', this.element); - - this.dragging = true; - this.target.fire(dragEvent); - - // reset active dropzones - this.activeDrops.dropzones = []; - this.activeDrops.elements = []; - this.activeDrops.rects = []; - - if (!this.dynamicDrop) { - this.setActiveDrops(this.element); - } - - var dropEvents = this.getDropEvents(event, dragEvent); - - if (dropEvents.activate) { - this.fireActiveDrops(dropEvents.activate); - } - - return dragEvent; - }, - - dragMove: function (event) { - var target = this.target, - dragEvent = new InteractEvent(this, event, 'drag', 'move', this.element), - draggableElement = this.element, - drop = this.getDrop(dragEvent, event, draggableElement); - - this.dropTarget = drop.dropzone; - this.dropElement = drop.element; - - var dropEvents = this.getDropEvents(event, dragEvent); - - target.fire(dragEvent); - - if (dropEvents.leave) { this.prevDropTarget.fire(dropEvents.leave); } - if (dropEvents.enter) { this.dropTarget.fire(dropEvents.enter); } - if (dropEvents.move ) { this.dropTarget.fire(dropEvents.move ); } - - this.prevDropTarget = this.dropTarget; - this.prevDropElement = this.dropElement; - - return dragEvent; - }, - - resizeStart: function (event) { - var resizeEvent = new InteractEvent(this, event, 'resize', 'start', this.element); - - if (this.prepared.edges) { - var startRect = this.target.getRect(this.element); - - /* - * When using the `resizable.square` or `resizable.preserveAspectRatio` options, resizing from one edge - * will affect another. E.g. with `resizable.square`, resizing to make the right edge larger will make - * the bottom edge larger by the same amount. We call these 'linked' edges. Any linked edges will depend - * on the active edges and the edge being interacted with. - */ - if (this.target.options.resize.square || this.target.options.resize.preserveAspectRatio) { - var linkedEdges = extend({}, this.prepared.edges); - - linkedEdges.top = linkedEdges.top || (linkedEdges.left && !linkedEdges.bottom); - linkedEdges.left = linkedEdges.left || (linkedEdges.top && !linkedEdges.right ); - linkedEdges.bottom = linkedEdges.bottom || (linkedEdges.right && !linkedEdges.top ); - linkedEdges.right = linkedEdges.right || (linkedEdges.bottom && !linkedEdges.left ); - - this.prepared._linkedEdges = linkedEdges; - } - else { - this.prepared._linkedEdges = null; - } - - // if using `resizable.preserveAspectRatio` option, record aspect ratio at the start of the resize - if (this.target.options.resize.preserveAspectRatio) { - this.resizeStartAspectRatio = startRect.width / startRect.height; - } - - this.resizeRects = { - start : startRect, - current : extend({}, startRect), - restricted: extend({}, startRect), - previous : extend({}, startRect), - delta : { - left: 0, right : 0, width : 0, - top : 0, bottom: 0, height: 0 - } - }; - - resizeEvent.rect = this.resizeRects.restricted; - resizeEvent.deltaRect = this.resizeRects.delta; - } - - this.target.fire(resizeEvent); - - this.resizing = true; - - return resizeEvent; - }, - - resizeMove: function (event) { - var resizeEvent = new InteractEvent(this, event, 'resize', 'move', this.element); - - var edges = this.prepared.edges, - invert = this.target.options.resize.invert, - invertible = invert === 'reposition' || invert === 'negate'; - - if (edges) { - var dx = resizeEvent.dx, - dy = resizeEvent.dy, - - start = this.resizeRects.start, - current = this.resizeRects.current, - restricted = this.resizeRects.restricted, - delta = this.resizeRects.delta, - previous = extend(this.resizeRects.previous, restricted), - - originalEdges = edges; - - // `resize.preserveAspectRatio` takes precedence over `resize.square` - if (this.target.options.resize.preserveAspectRatio) { - var resizeStartAspectRatio = this.resizeStartAspectRatio; - - edges = this.prepared._linkedEdges; - - if ((originalEdges.left && originalEdges.bottom) - || (originalEdges.right && originalEdges.top)) { - dy = -dx / resizeStartAspectRatio; - } - else if (originalEdges.left || originalEdges.right) { dy = dx / resizeStartAspectRatio; } - else if (originalEdges.top || originalEdges.bottom) { dx = dy * resizeStartAspectRatio; } - } - else if (this.target.options.resize.square) { - edges = this.prepared._linkedEdges; - - if ((originalEdges.left && originalEdges.bottom) - || (originalEdges.right && originalEdges.top)) { - dy = -dx; - } - else if (originalEdges.left || originalEdges.right) { dy = dx; } - else if (originalEdges.top || originalEdges.bottom) { dx = dy; } - } - - // update the 'current' rect without modifications - if (edges.top ) { current.top += dy; } - if (edges.bottom) { current.bottom += dy; } - if (edges.left ) { current.left += dx; } - if (edges.right ) { current.right += dx; } - - if (invertible) { - // if invertible, copy the current rect - extend(restricted, current); - - if (invert === 'reposition') { - // swap edge values if necessary to keep width/height positive - var swap; - - if (restricted.top > restricted.bottom) { - swap = restricted.top; - - restricted.top = restricted.bottom; - restricted.bottom = swap; - } - if (restricted.left > restricted.right) { - swap = restricted.left; - - restricted.left = restricted.right; - restricted.right = swap; - } - } - } - else { - // if not invertible, restrict to minimum of 0x0 rect - restricted.top = Math.min(current.top, start.bottom); - restricted.bottom = Math.max(current.bottom, start.top); - restricted.left = Math.min(current.left, start.right); - restricted.right = Math.max(current.right, start.left); - } - - restricted.width = restricted.right - restricted.left; - restricted.height = restricted.bottom - restricted.top ; - - for (var edge in restricted) { - delta[edge] = restricted[edge] - previous[edge]; - } - - resizeEvent.edges = this.prepared.edges; - resizeEvent.rect = restricted; - resizeEvent.deltaRect = delta; - } - - this.target.fire(resizeEvent); - - return resizeEvent; - }, - - gestureStart: function (event) { - var gestureEvent = new InteractEvent(this, event, 'gesture', 'start', this.element); - - gestureEvent.ds = 0; - - this.gesture.startDistance = this.gesture.prevDistance = gestureEvent.distance; - this.gesture.startAngle = this.gesture.prevAngle = gestureEvent.angle; - this.gesture.scale = 1; - - this.gesturing = true; - - this.target.fire(gestureEvent); - - return gestureEvent; - }, - - gestureMove: function (event) { - if (!this.pointerIds.length) { - return this.prevEvent; - } - - var gestureEvent; - - gestureEvent = new InteractEvent(this, event, 'gesture', 'move', this.element); - gestureEvent.ds = gestureEvent.scale - this.gesture.scale; - - this.target.fire(gestureEvent); - - this.gesture.prevAngle = gestureEvent.angle; - this.gesture.prevDistance = gestureEvent.distance; - - if (gestureEvent.scale !== Infinity && - gestureEvent.scale !== null && - gestureEvent.scale !== undefined && - !isNaN(gestureEvent.scale)) { - - this.gesture.scale = gestureEvent.scale; - } - - return gestureEvent; - }, - - pointerHold: function (pointer, event, eventTarget) { - this.collectEventTargets(pointer, event, eventTarget, 'hold'); - }, - - pointerUp: function (pointer, event, eventTarget, curEventTarget) { - var pointerIndex = this.mouse? 0 : indexOf(this.pointerIds, getPointerId(pointer)); - - clearTimeout(this.holdTimers[pointerIndex]); - - this.collectEventTargets(pointer, event, eventTarget, 'up' ); - this.collectEventTargets(pointer, event, eventTarget, 'tap'); - - this.pointerEnd(pointer, event, eventTarget, curEventTarget); - - this.removePointer(pointer); - }, - - pointerCancel: function (pointer, event, eventTarget, curEventTarget) { - var pointerIndex = this.mouse? 0 : indexOf(this.pointerIds, getPointerId(pointer)); - - clearTimeout(this.holdTimers[pointerIndex]); - - this.collectEventTargets(pointer, event, eventTarget, 'cancel'); - this.pointerEnd(pointer, event, eventTarget, curEventTarget); - - this.removePointer(pointer); - }, - - // http://www.quirksmode.org/dom/events/click.html - // >Events leading to dblclick - // - // IE8 doesn't fire down event before dblclick. - // This workaround tries to fire a tap and doubletap after dblclick - ie8Dblclick: function (pointer, event, eventTarget) { - if (this.prevTap - && event.clientX === this.prevTap.clientX - && event.clientY === this.prevTap.clientY - && eventTarget === this.prevTap.target) { - - this.downTargets[0] = eventTarget; - this.downTimes[0] = new Date().getTime(); - this.collectEventTargets(pointer, event, eventTarget, 'tap'); - } - }, - - // End interact move events and stop auto-scroll unless inertia is enabled - pointerEnd: function (pointer, event, eventTarget, curEventTarget) { - var endEvent, - target = this.target, - options = target && target.options, - inertiaOptions = options && this.prepared.name && options[this.prepared.name].inertia, - inertiaStatus = this.inertiaStatus; - - if (this.interacting()) { - - if (inertiaStatus.active && !inertiaStatus.ending) { return; } - - var pointerSpeed, - now = new Date().getTime(), - inertiaPossible = false, - inertia = false, - smoothEnd = false, - endSnap = checkSnap(target, this.prepared.name) && options[this.prepared.name].snap.endOnly, - endRestrict = checkRestrict(target, this.prepared.name) && options[this.prepared.name].restrict.endOnly, - dx = 0, - dy = 0, - startEvent; - - if (this.dragging) { - if (options.drag.axis === 'x' ) { pointerSpeed = Math.abs(this.pointerDelta.client.vx); } - else if (options.drag.axis === 'y' ) { pointerSpeed = Math.abs(this.pointerDelta.client.vy); } - else /*options.drag.axis === 'xy'*/{ pointerSpeed = this.pointerDelta.client.speed; } - } - else { - pointerSpeed = this.pointerDelta.client.speed; - } - - // check if inertia should be started - inertiaPossible = (inertiaOptions && inertiaOptions.enabled - && this.prepared.name !== 'gesture' - && event !== inertiaStatus.startEvent); - - inertia = (inertiaPossible - && (now - this.curCoords.timeStamp) < 50 - && pointerSpeed > inertiaOptions.minSpeed - && pointerSpeed > inertiaOptions.endSpeed); - - if (inertiaPossible && !inertia && (endSnap || endRestrict)) { - - var snapRestrict = {}; - - snapRestrict.snap = snapRestrict.restrict = snapRestrict; - - if (endSnap) { - this.setSnapping(this.curCoords.page, snapRestrict); - if (snapRestrict.locked) { - dx += snapRestrict.dx; - dy += snapRestrict.dy; - } - } - - if (endRestrict) { - this.setRestriction(this.curCoords.page, snapRestrict); - if (snapRestrict.restricted) { - dx += snapRestrict.dx; - dy += snapRestrict.dy; - } - } - - if (dx || dy) { - smoothEnd = true; - } - } - - if (inertia || smoothEnd) { - copyCoords(inertiaStatus.upCoords, this.curCoords); - - this.pointers[0] = inertiaStatus.startEvent = startEvent = - new InteractEvent(this, event, this.prepared.name, 'inertiastart', this.element); - - inertiaStatus.t0 = now; - - target.fire(inertiaStatus.startEvent); - - if (inertia) { - inertiaStatus.vx0 = this.pointerDelta.client.vx; - inertiaStatus.vy0 = this.pointerDelta.client.vy; - inertiaStatus.v0 = pointerSpeed; - - this.calcInertia(inertiaStatus); - - var page = extend({}, this.curCoords.page), - origin = getOriginXY(target, this.element), - statusObject; - - page.x = page.x + inertiaStatus.xe - origin.x; - page.y = page.y + inertiaStatus.ye - origin.y; - - statusObject = { - useStatusXY: true, - x: page.x, - y: page.y, - dx: 0, - dy: 0, - snap: null - }; - - statusObject.snap = statusObject; - - dx = dy = 0; - - if (endSnap) { - var snap = this.setSnapping(this.curCoords.page, statusObject); - - if (snap.locked) { - dx += snap.dx; - dy += snap.dy; - } - } - - if (endRestrict) { - var restrict = this.setRestriction(this.curCoords.page, statusObject); - - if (restrict.restricted) { - dx += restrict.dx; - dy += restrict.dy; - } - } - - inertiaStatus.modifiedXe += dx; - inertiaStatus.modifiedYe += dy; - - inertiaStatus.i = reqFrame(this.boundInertiaFrame); - } - else { - inertiaStatus.smoothEnd = true; - inertiaStatus.xe = dx; - inertiaStatus.ye = dy; - - inertiaStatus.sx = inertiaStatus.sy = 0; - - inertiaStatus.i = reqFrame(this.boundSmoothEndFrame); - } - - inertiaStatus.active = true; - return; - } - - if (endSnap || endRestrict) { - // fire a move event at the snapped coordinates - this.pointerMove(pointer, event, eventTarget, curEventTarget, true); - } - } - - if (this.dragging) { - endEvent = new InteractEvent(this, event, 'drag', 'end', this.element); - - var draggableElement = this.element, - drop = this.getDrop(endEvent, event, draggableElement); - - this.dropTarget = drop.dropzone; - this.dropElement = drop.element; - - var dropEvents = this.getDropEvents(event, endEvent); - - if (dropEvents.leave) { this.prevDropTarget.fire(dropEvents.leave); } - if (dropEvents.enter) { this.dropTarget.fire(dropEvents.enter); } - if (dropEvents.drop ) { this.dropTarget.fire(dropEvents.drop ); } - if (dropEvents.deactivate) { - this.fireActiveDrops(dropEvents.deactivate); - } - - target.fire(endEvent); - } - else if (this.resizing) { - endEvent = new InteractEvent(this, event, 'resize', 'end', this.element); - target.fire(endEvent); - } - else if (this.gesturing) { - endEvent = new InteractEvent(this, event, 'gesture', 'end', this.element); - target.fire(endEvent); - } - - this.stop(event); - }, - - collectDrops: function (element) { - var drops = [], - elements = [], - i; - - element = element || this.element; - - // collect all dropzones and their elements which qualify for a drop - for (i = 0; i < interactables.length; i++) { - if (!interactables[i].options.drop.enabled) { continue; } - - var current = interactables[i], - accept = current.options.drop.accept; - - // test the draggable element against the dropzone's accept setting - if ((isElement(accept) && accept !== element) - || (isString(accept) - && !matchesSelector(element, accept))) { - - continue; - } - - // query for new elements if necessary - var dropElements = current.selector? current._context.querySelectorAll(current.selector) : [current._element]; - - for (var j = 0, len = dropElements.length; j < len; j++) { - var currentElement = dropElements[j]; - - if (currentElement === element) { - continue; - } - - drops.push(current); - elements.push(currentElement); - } - } - - return { - dropzones: drops, - elements: elements - }; - }, - - fireActiveDrops: function (event) { - var i, - current, - currentElement, - prevElement; - - // loop through all active dropzones and trigger event - for (i = 0; i < this.activeDrops.dropzones.length; i++) { - current = this.activeDrops.dropzones[i]; - currentElement = this.activeDrops.elements [i]; - - // prevent trigger of duplicate events on same element - if (currentElement !== prevElement) { - // set current element as event target - event.target = currentElement; - current.fire(event); - } - prevElement = currentElement; - } - }, - - // Collect a new set of possible drops and save them in activeDrops. - // setActiveDrops should always be called when a drag has just started or a - // drag event happens while dynamicDrop is true - setActiveDrops: function (dragElement) { - // get dropzones and their elements that could receive the draggable - var possibleDrops = this.collectDrops(dragElement, true); - - this.activeDrops.dropzones = possibleDrops.dropzones; - this.activeDrops.elements = possibleDrops.elements; - this.activeDrops.rects = []; - - for (var i = 0; i < this.activeDrops.dropzones.length; i++) { - this.activeDrops.rects[i] = this.activeDrops.dropzones[i].getRect(this.activeDrops.elements[i]); - } - }, - - getDrop: function (dragEvent, event, dragElement) { - var validDrops = []; - - if (dynamicDrop) { - this.setActiveDrops(dragElement); - } - - // collect all dropzones and their elements which qualify for a drop - for (var j = 0; j < this.activeDrops.dropzones.length; j++) { - var current = this.activeDrops.dropzones[j], - currentElement = this.activeDrops.elements [j], - rect = this.activeDrops.rects [j]; - - validDrops.push(current.dropCheck(dragEvent, event, this.target, dragElement, currentElement, rect) - ? currentElement - : null); - } - - // get the most appropriate dropzone based on DOM depth and order - var dropIndex = indexOfDeepestElement(validDrops), - dropzone = this.activeDrops.dropzones[dropIndex] || null, - element = this.activeDrops.elements [dropIndex] || null; - - return { - dropzone: dropzone, - element: element - }; - }, - - getDropEvents: function (pointerEvent, dragEvent) { - var dropEvents = { - enter : null, - leave : null, - activate : null, - deactivate: null, - move : null, - drop : null - }; - - if (this.dropElement !== this.prevDropElement) { - // if there was a prevDropTarget, create a dragleave event - if (this.prevDropTarget) { - dropEvents.leave = { - target : this.prevDropElement, - dropzone : this.prevDropTarget, - relatedTarget: dragEvent.target, - draggable : dragEvent.interactable, - dragEvent : dragEvent, - interaction : this, - timeStamp : dragEvent.timeStamp, - type : 'dragleave' - }; - - dragEvent.dragLeave = this.prevDropElement; - dragEvent.prevDropzone = this.prevDropTarget; - } - // if the dropTarget is not null, create a dragenter event - if (this.dropTarget) { - dropEvents.enter = { - target : this.dropElement, - dropzone : this.dropTarget, - relatedTarget: dragEvent.target, - draggable : dragEvent.interactable, - dragEvent : dragEvent, - interaction : this, - timeStamp : dragEvent.timeStamp, - type : 'dragenter' - }; - - dragEvent.dragEnter = this.dropElement; - dragEvent.dropzone = this.dropTarget; - } - } - - if (dragEvent.type === 'dragend' && this.dropTarget) { - dropEvents.drop = { - target : this.dropElement, - dropzone : this.dropTarget, - relatedTarget: dragEvent.target, - draggable : dragEvent.interactable, - dragEvent : dragEvent, - interaction : this, - timeStamp : dragEvent.timeStamp, - type : 'drop' - }; - - dragEvent.dropzone = this.dropTarget; - } - if (dragEvent.type === 'dragstart') { - dropEvents.activate = { - target : null, - dropzone : null, - relatedTarget: dragEvent.target, - draggable : dragEvent.interactable, - dragEvent : dragEvent, - interaction : this, - timeStamp : dragEvent.timeStamp, - type : 'dropactivate' - }; - } - if (dragEvent.type === 'dragend') { - dropEvents.deactivate = { - target : null, - dropzone : null, - relatedTarget: dragEvent.target, - draggable : dragEvent.interactable, - dragEvent : dragEvent, - interaction : this, - timeStamp : dragEvent.timeStamp, - type : 'dropdeactivate' - }; - } - if (dragEvent.type === 'dragmove' && this.dropTarget) { - dropEvents.move = { - target : this.dropElement, - dropzone : this.dropTarget, - relatedTarget: dragEvent.target, - draggable : dragEvent.interactable, - dragEvent : dragEvent, - interaction : this, - dragmove : dragEvent, - timeStamp : dragEvent.timeStamp, - type : 'dropmove' - }; - dragEvent.dropzone = this.dropTarget; - } - - return dropEvents; - }, - - currentAction: function () { - return (this.dragging && 'drag') || (this.resizing && 'resize') || (this.gesturing && 'gesture') || null; - }, - - interacting: function () { - return this.dragging || this.resizing || this.gesturing; - }, - - clearTargets: function () { - this.target = this.element = null; - - this.dropTarget = this.dropElement = this.prevDropTarget = this.prevDropElement = null; - }, - - stop: function (event) { - if (this.interacting()) { - autoScroll.stop(); - this.matches = []; - this.matchElements = []; - - var target = this.target; - - if (target.options.styleCursor) { - target._doc.documentElement.style.cursor = ''; - } - - // prevent Default only if were previously interacting - if (event && isFunction(event.preventDefault)) { - this.checkAndPreventDefault(event, target, this.element); - } - - if (this.dragging) { - this.activeDrops.dropzones = this.activeDrops.elements = this.activeDrops.rects = null; - } - } - - this.clearTargets(); - - this.pointerIsDown = this.snapStatus.locked = this.dragging = this.resizing = this.gesturing = false; - this.prepared.name = this.prevEvent = null; - this.inertiaStatus.resumeDx = this.inertiaStatus.resumeDy = 0; - - // remove pointers if their ID isn't in this.pointerIds - for (var i = 0; i < this.pointers.length; i++) { - if (indexOf(this.pointerIds, getPointerId(this.pointers[i])) === -1) { - this.pointers.splice(i, 1); - } - } - }, - - inertiaFrame: function () { - var inertiaStatus = this.inertiaStatus, - options = this.target.options[this.prepared.name].inertia, - lambda = options.resistance, - t = new Date().getTime() / 1000 - inertiaStatus.t0; - - if (t < inertiaStatus.te) { - - var progress = 1 - (Math.exp(-lambda * t) - inertiaStatus.lambda_v0) / inertiaStatus.one_ve_v0; - - if (inertiaStatus.modifiedXe === inertiaStatus.xe && inertiaStatus.modifiedYe === inertiaStatus.ye) { - inertiaStatus.sx = inertiaStatus.xe * progress; - inertiaStatus.sy = inertiaStatus.ye * progress; - } - else { - var quadPoint = getQuadraticCurvePoint( - 0, 0, - inertiaStatus.xe, inertiaStatus.ye, - inertiaStatus.modifiedXe, inertiaStatus.modifiedYe, - progress); - - inertiaStatus.sx = quadPoint.x; - inertiaStatus.sy = quadPoint.y; - } - - this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); - - inertiaStatus.i = reqFrame(this.boundInertiaFrame); - } - else { - inertiaStatus.ending = true; - - inertiaStatus.sx = inertiaStatus.modifiedXe; - inertiaStatus.sy = inertiaStatus.modifiedYe; - - this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); - this.pointerEnd(inertiaStatus.startEvent, inertiaStatus.startEvent); - - inertiaStatus.active = inertiaStatus.ending = false; - } - }, - - smoothEndFrame: function () { - var inertiaStatus = this.inertiaStatus, - t = new Date().getTime() - inertiaStatus.t0, - duration = this.target.options[this.prepared.name].inertia.smoothEndDuration; - - if (t < duration) { - inertiaStatus.sx = easeOutQuad(t, 0, inertiaStatus.xe, duration); - inertiaStatus.sy = easeOutQuad(t, 0, inertiaStatus.ye, duration); - - this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); - - inertiaStatus.i = reqFrame(this.boundSmoothEndFrame); - } - else { - inertiaStatus.ending = true; - - inertiaStatus.sx = inertiaStatus.xe; - inertiaStatus.sy = inertiaStatus.ye; - - this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); - this.pointerEnd(inertiaStatus.startEvent, inertiaStatus.startEvent); - - inertiaStatus.smoothEnd = - inertiaStatus.active = inertiaStatus.ending = false; - } - }, - - addPointer: function (pointer) { - var id = getPointerId(pointer), - index = this.mouse? 0 : indexOf(this.pointerIds, id); - - if (index === -1) { - index = this.pointerIds.length; - } - - this.pointerIds[index] = id; - this.pointers[index] = pointer; - - return index; - }, - - removePointer: function (pointer) { - var id = getPointerId(pointer), - index = this.mouse? 0 : indexOf(this.pointerIds, id); - - if (index === -1) { return; } - - this.pointers .splice(index, 1); - this.pointerIds .splice(index, 1); - this.downTargets.splice(index, 1); - this.downTimes .splice(index, 1); - this.holdTimers .splice(index, 1); - }, - - recordPointer: function (pointer) { - var index = this.mouse? 0: indexOf(this.pointerIds, getPointerId(pointer)); - - if (index === -1) { return; } - - this.pointers[index] = pointer; - }, - - collectEventTargets: function (pointer, event, eventTarget, eventType) { - var pointerIndex = this.mouse? 0 : indexOf(this.pointerIds, getPointerId(pointer)); - - // do not fire a tap event if the pointer was moved before being lifted - if (eventType === 'tap' && (this.pointerWasMoved - // or if the pointerup target is different to the pointerdown target - || !(this.downTargets[pointerIndex] && this.downTargets[pointerIndex] === eventTarget))) { - return; - } - - var targets = [], - elements = [], - element = eventTarget; - - function collectSelectors (interactable, selector, context) { - var els = ie8MatchesSelector - ? context.querySelectorAll(selector) - : undefined; - - if (interactable._iEvents[eventType] - && isElement(element) - && inContext(interactable, element) - && !testIgnore(interactable, element, eventTarget) - && testAllow(interactable, element, eventTarget) - && matchesSelector(element, selector, els)) { - - targets.push(interactable); - elements.push(element); - } - } - - while (element) { - if (interact.isSet(element) && interact(element)._iEvents[eventType]) { - targets.push(interact(element)); - elements.push(element); - } - - interactables.forEachSelector(collectSelectors); - - element = parentElement(element); - } - - // create the tap event even if there are no listeners so that - // doubletap can still be created and fired - if (targets.length || eventType === 'tap') { - this.firePointers(pointer, event, eventTarget, targets, elements, eventType); - } - }, - - firePointers: function (pointer, event, eventTarget, targets, elements, eventType) { - var pointerIndex = this.mouse? 0 : indexOf(this.pointerIds, getPointerId(pointer)), - pointerEvent = {}, - i, - // for tap events - interval, createNewDoubleTap; - - // if it's a doubletap then the event properties would have been - // copied from the tap event and provided as the pointer argument - if (eventType === 'doubletap') { - pointerEvent = pointer; - } - else { - pointerExtend(pointerEvent, event); - if (event !== pointer) { - pointerExtend(pointerEvent, pointer); - } - - pointerEvent.preventDefault = preventOriginalDefault; - pointerEvent.stopPropagation = InteractEvent.prototype.stopPropagation; - pointerEvent.stopImmediatePropagation = InteractEvent.prototype.stopImmediatePropagation; - pointerEvent.interaction = this; - - pointerEvent.timeStamp = new Date().getTime(); - pointerEvent.originalEvent = event; - pointerEvent.originalPointer = pointer; - pointerEvent.type = eventType; - pointerEvent.pointerId = getPointerId(pointer); - pointerEvent.pointerType = this.mouse? 'mouse' : !supportsPointerEvent? 'touch' - : isString(pointer.pointerType) - ? pointer.pointerType - : [,,'touch', 'pen', 'mouse'][pointer.pointerType]; - } - - if (eventType === 'tap') { - pointerEvent.dt = pointerEvent.timeStamp - this.downTimes[pointerIndex]; - - interval = pointerEvent.timeStamp - this.tapTime; - createNewDoubleTap = !!(this.prevTap && this.prevTap.type !== 'doubletap' - && this.prevTap.target === pointerEvent.target - && interval < 500); - - pointerEvent.double = createNewDoubleTap; - - this.tapTime = pointerEvent.timeStamp; - } - - for (i = 0; i < targets.length; i++) { - pointerEvent.currentTarget = elements[i]; - pointerEvent.interactable = targets[i]; - targets[i].fire(pointerEvent); - - if (pointerEvent.immediatePropagationStopped - ||(pointerEvent.propagationStopped && elements[i + 1] !== pointerEvent.currentTarget)) { - break; - } - } - - if (createNewDoubleTap) { - var doubleTap = {}; - - extend(doubleTap, pointerEvent); - - doubleTap.dt = interval; - doubleTap.type = 'doubletap'; - - this.collectEventTargets(doubleTap, event, eventTarget, 'doubletap'); - - this.prevTap = doubleTap; - } - else if (eventType === 'tap') { - this.prevTap = pointerEvent; - } - }, - - validateSelector: function (pointer, event, matches, matchElements) { - for (var i = 0, len = matches.length; i < len; i++) { - var match = matches[i], - matchElement = matchElements[i], - action = validateAction(match.getAction(pointer, event, this, matchElement), match); - - if (action && withinInteractionLimit(match, matchElement, action)) { - this.target = match; - this.element = matchElement; - - return action; - } - } - }, - - setSnapping: function (pageCoords, status) { - var snap = this.target.options[this.prepared.name].snap, - targets = [], - target, - page, - i; - - status = status || this.snapStatus; - - if (status.useStatusXY) { - page = { x: status.x, y: status.y }; - } - else { - var origin = getOriginXY(this.target, this.element); - - page = extend({}, pageCoords); - - page.x -= origin.x; - page.y -= origin.y; - } - - status.realX = page.x; - status.realY = page.y; - - page.x = page.x - this.inertiaStatus.resumeDx; - page.y = page.y - this.inertiaStatus.resumeDy; - - var len = snap.targets? snap.targets.length : 0; - - for (var relIndex = 0; relIndex < this.snapOffsets.length; relIndex++) { - var relative = { - x: page.x - this.snapOffsets[relIndex].x, - y: page.y - this.snapOffsets[relIndex].y - }; - - for (i = 0; i < len; i++) { - if (isFunction(snap.targets[i])) { - target = snap.targets[i](relative.x, relative.y, this); - } - else { - target = snap.targets[i]; - } - - if (!target) { continue; } - - targets.push({ - x: isNumber(target.x) ? (target.x + this.snapOffsets[relIndex].x) : relative.x, - y: isNumber(target.y) ? (target.y + this.snapOffsets[relIndex].y) : relative.y, - - range: isNumber(target.range)? target.range: snap.range - }); - } - } - - var closest = { - target: null, - inRange: false, - distance: 0, - range: 0, - dx: 0, - dy: 0 - }; - - for (i = 0, len = targets.length; i < len; i++) { - target = targets[i]; - - var range = target.range, - dx = target.x - page.x, - dy = target.y - page.y, - distance = hypot(dx, dy), - 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))) { - - if (range === Infinity) { - inRange = true; - } - - closest.target = target; - closest.distance = distance; - closest.range = range; - closest.inRange = inRange; - closest.dx = dx; - closest.dy = dy; - - status.range = range; - } - } - - var snapChanged; - - if (closest.target) { - snapChanged = (status.snappedX !== closest.target.x || status.snappedY !== closest.target.y); - - status.snappedX = closest.target.x; - status.snappedY = closest.target.y; - } - else { - snapChanged = true; - - status.snappedX = NaN; - status.snappedY = NaN; - } - - status.dx = closest.dx; - status.dy = closest.dy; - - status.changed = (snapChanged || (closest.inRange && !status.locked)); - status.locked = closest.inRange; - - return status; - }, - - setRestriction: function (pageCoords, status) { - var target = this.target, - restrict = target && target.options[this.prepared.name].restrict, - restriction = restrict && restrict.restriction, - page; - - if (!restriction) { - return status; - } - - status = status || this.restrictStatus; - - page = status.useStatusXY - ? page = { x: status.x, y: status.y } - : page = extend({}, pageCoords); - - if (status.snap && status.snap.locked) { - page.x += status.snap.dx || 0; - page.y += status.snap.dy || 0; - } - - page.x -= this.inertiaStatus.resumeDx; - page.y -= this.inertiaStatus.resumeDy; - - status.dx = 0; - status.dy = 0; - status.restricted = false; - - var rect, restrictedX, restrictedY; - - if (isString(restriction)) { - if (restriction === 'parent') { - restriction = parentElement(this.element); - } - else if (restriction === 'self') { - restriction = target.getRect(this.element); - } - else { - restriction = closest(this.element, restriction); - } - - if (!restriction) { return status; } - } - - if (isFunction(restriction)) { - restriction = restriction(page.x, page.y, this.element); - } - - if (isElement(restriction)) { - restriction = getElementRect(restriction); - } - - rect = restriction; - - if (!restriction) { - restrictedX = page.x; - restrictedY = page.y; - } - // object is assumed to have - // x, y, width, height or - // left, top, right, bottom - else if ('x' in restriction && 'y' in restriction) { - restrictedX = Math.max(Math.min(rect.x + rect.width - this.restrictOffset.right , page.x), rect.x + this.restrictOffset.left); - restrictedY = Math.max(Math.min(rect.y + rect.height - this.restrictOffset.bottom, page.y), rect.y + this.restrictOffset.top ); - } - else { - restrictedX = Math.max(Math.min(rect.right - this.restrictOffset.right , page.x), rect.left + this.restrictOffset.left); - restrictedY = Math.max(Math.min(rect.bottom - this.restrictOffset.bottom, page.y), rect.top + this.restrictOffset.top ); - } - - status.dx = restrictedX - page.x; - status.dy = restrictedY - page.y; - - status.changed = status.restrictedX !== restrictedX || status.restrictedY !== restrictedY; - status.restricted = !!(status.dx || status.dy); - - status.restrictedX = restrictedX; - status.restrictedY = restrictedY; - - return status; - }, - - checkAndPreventDefault: function (event, interactable, element) { - if (!(interactable = interactable || this.target)) { return; } - - var options = interactable.options, - prevent = options.preventDefault; - - if (prevent === 'auto' && element && !/^(input|select|textarea)$/i.test(event.target.nodeName)) { - // do not preventDefault on pointerdown if the prepared action is a drag - // and dragging can only start from a certain direction - this allows - // a touch to pan the viewport if a drag isn't in the right direction - if (/down|start/i.test(event.type) - && this.prepared.name === 'drag' && options.drag.axis !== 'xy') { - - return; - } - - // with manualStart, only preventDefault while interacting - if (options[this.prepared.name] && options[this.prepared.name].manualStart - && !this.interacting()) { - return; - } - - event.preventDefault(); - return; - } - - if (prevent === 'always') { - event.preventDefault(); - return; - } - }, - - calcInertia: function (status) { - var inertiaOptions = this.target.options[this.prepared.name].inertia, - lambda = inertiaOptions.resistance, - inertiaDur = -Math.log(inertiaOptions.endSpeed / status.v0) / lambda; - - status.x0 = this.prevEvent.pageX; - status.y0 = this.prevEvent.pageY; - status.t0 = status.startEvent.timeStamp / 1000; - status.sx = status.sy = 0; - - status.modifiedXe = status.xe = (status.vx0 - inertiaDur) / lambda; - status.modifiedYe = status.ye = (status.vy0 - inertiaDur) / lambda; - status.te = inertiaDur; - - status.lambda_v0 = lambda / status.v0; - status.one_ve_v0 = 1 - inertiaOptions.endSpeed / status.v0; - }, - - autoScrollMove: function (pointer) { - if (!(this.interacting() - && checkAutoScroll(this.target, this.prepared.name))) { - return; - } - - if (this.inertiaStatus.active) { - autoScroll.x = autoScroll.y = 0; - return; - } - - var top, - right, - bottom, - left, - options = this.target.options[this.prepared.name].autoScroll, - container = options.container || getWindow(this.element); - - if (isWindow(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 { - var rect = 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(this); - } - }, - - _updateEventTargets: function (target, currentTarget) { - this._eventTarget = target; - this._curEventTarget = currentTarget; - } - - }; - - function getInteractionFromPointer (pointer, eventType, eventTarget) { - var i = 0, len = interactions.length, - mouseEvent = (/mouse/i.test(pointer.pointerType || eventType) - // MSPointerEvent.MSPOINTER_TYPE_MOUSE - || pointer.pointerType === 4), - interaction; - - var id = getPointerId(pointer); - - // try to resume inertia with a new pointer - if (/down|start/i.test(eventType)) { - for (i = 0; i < len; i++) { - interaction = interactions[i]; - - var element = eventTarget; - - if (interaction.inertiaStatus.active && interaction.target.options[interaction.prepared.name].inertia.allowResume - && (interaction.mouse === mouseEvent)) { - while (element) { - // if the element is the interaction element - if (element === interaction.element) { - return interaction; - } - element = parentElement(element); - } - } - } - } - - // if it's a mouse interaction - if (mouseEvent || !(supportsTouch || supportsPointerEvent)) { - - // find a mouse interaction that's not in inertia phase - for (i = 0; i < len; i++) { - if (interactions[i].mouse && !interactions[i].inertiaStatus.active) { - return interactions[i]; - } - } - - // find any interaction specifically for mouse. - // if the eventType is a mousedown, and inertia is active - // ignore the interaction - for (i = 0; i < len; i++) { - if (interactions[i].mouse && !(/down/.test(eventType) && interactions[i].inertiaStatus.active)) { - return interaction; - } - } - - // create a new interaction for mouse - interaction = new Interaction(); - interaction.mouse = true; - - return interaction; - } - - // get interaction that has this pointer - for (i = 0; i < len; i++) { - if (contains(interactions[i].pointerIds, id)) { - return interactions[i]; - } - } - - // at this stage, a pointerUp should not return an interaction - if (/up|end|out/i.test(eventType)) { - return null; - } - - // get first idle interaction - for (i = 0; i < len; i++) { - interaction = interactions[i]; - - if ((!interaction.prepared.name || (interaction.target.options.gesture.enabled)) - && !interaction.interacting() - && !(!mouseEvent && interaction.mouse)) { - - return interaction; - } - } - - return new Interaction(); - } - - function doOnInteractions (method) { - return (function (event) { - var interaction, - eventTarget = getActualElement(event.path - ? event.path[0] - : event.target), - curEventTarget = getActualElement(event.currentTarget), - i; - - if (supportsTouch && /touch/.test(event.type)) { - prevTouchTime = new Date().getTime(); - - for (i = 0; i < event.changedTouches.length; i++) { - var pointer = event.changedTouches[i]; - - interaction = getInteractionFromPointer(pointer, event.type, eventTarget); - - if (!interaction) { continue; } - - interaction._updateEventTargets(eventTarget, curEventTarget); - - interaction[method](pointer, event, eventTarget, curEventTarget); - } - } - else { - if (!supportsPointerEvent && /mouse/.test(event.type)) { - // ignore mouse events while touch interactions are active - for (i = 0; i < interactions.length; i++) { - if (!interactions[i].mouse && interactions[i].pointerIsDown) { - return; - } - } - - // try to ignore mouse events that are simulated by the browser - // after a touch event - if (new Date().getTime() - prevTouchTime < 500) { - return; - } - } - - interaction = getInteractionFromPointer(event, event.type, eventTarget); - - if (!interaction) { return; } - - interaction._updateEventTargets(eventTarget, curEventTarget); - - interaction[method](event, event, eventTarget, curEventTarget); - } - }); - } - - function InteractEvent (interaction, event, action, phase, element, related) { - var client, - page, - target = interaction.target, - snapStatus = interaction.snapStatus, - restrictStatus = interaction.restrictStatus, - pointers = interaction.pointers, - deltaSource = (target && target.options || defaultOptions).deltaSource, - sourceX = deltaSource + 'X', - sourceY = deltaSource + 'Y', - options = target? target.options: defaultOptions, - origin = getOriginXY(target, element), - starting = phase === 'start', - ending = phase === 'end', - coords = starting? interaction.startCoords : interaction.curCoords; - - element = element || interaction.element; - - page = extend({}, coords.page); - client = extend({}, coords.client); - - page.x -= origin.x; - page.y -= origin.y; - - client.x -= origin.x; - client.y -= origin.y; - - var relativePoints = options[action].snap && options[action].snap.relativePoints ; - - if (checkSnap(target, action) && !(starting && relativePoints && relativePoints.length)) { - this.snap = { - range : snapStatus.range, - locked : snapStatus.locked, - x : snapStatus.snappedX, - y : snapStatus.snappedY, - realX : snapStatus.realX, - realY : snapStatus.realY, - dx : snapStatus.dx, - dy : snapStatus.dy - }; - - if (snapStatus.locked) { - page.x += snapStatus.dx; - page.y += snapStatus.dy; - client.x += snapStatus.dx; - client.y += snapStatus.dy; - } - } - - if (checkRestrict(target, action) && !(starting && options[action].restrict.elementRect) && restrictStatus.restricted) { - page.x += restrictStatus.dx; - page.y += restrictStatus.dy; - client.x += restrictStatus.dx; - client.y += restrictStatus.dy; - - this.restrict = { - dx: restrictStatus.dx, - dy: restrictStatus.dy - }; - } - - this.pageX = page.x; - this.pageY = page.y; - this.clientX = client.x; - this.clientY = client.y; - - this.x0 = interaction.startCoords.page.x - origin.x; - this.y0 = interaction.startCoords.page.y - origin.y; - this.clientX0 = interaction.startCoords.client.x - origin.x; - this.clientY0 = interaction.startCoords.client.y - origin.y; - this.ctrlKey = event.ctrlKey; - this.altKey = event.altKey; - this.shiftKey = event.shiftKey; - this.metaKey = event.metaKey; - this.button = event.button; - this.buttons = event.buttons; - this.target = element; - this.t0 = interaction.downTimes[0]; - this.type = action + (phase || ''); - - this.interaction = interaction; - this.interactable = target; - - var inertiaStatus = interaction.inertiaStatus; - - if (inertiaStatus.active) { - this.detail = 'inertia'; - } - - if (related) { - this.relatedTarget = related; - } - - // end event dx, dy is difference between start and end points - if (ending) { - if (deltaSource === 'client') { - this.dx = client.x - interaction.startCoords.client.x; - this.dy = client.y - interaction.startCoords.client.y; - } - else { - this.dx = page.x - interaction.startCoords.page.x; - this.dy = page.y - interaction.startCoords.page.y; - } - } - else if (starting) { - this.dx = 0; - this.dy = 0; - } - // copy properties from previousmove if starting inertia - else if (phase === 'inertiastart') { - this.dx = interaction.prevEvent.dx; - this.dy = interaction.prevEvent.dy; - } - else { - if (deltaSource === 'client') { - this.dx = client.x - interaction.prevEvent.clientX; - this.dy = client.y - interaction.prevEvent.clientY; - } - else { - this.dx = page.x - interaction.prevEvent.pageX; - this.dy = page.y - interaction.prevEvent.pageY; - } - } - if (interaction.prevEvent && interaction.prevEvent.detail === 'inertia' - && !inertiaStatus.active - && options[action].inertia && options[action].inertia.zeroResumeDelta) { - - inertiaStatus.resumeDx += this.dx; - inertiaStatus.resumeDy += this.dy; - - this.dx = this.dy = 0; - } - - if (action === 'resize' && interaction.resizeAxes) { - if (options.resize.square) { - if (interaction.resizeAxes === 'y') { - this.dx = this.dy; - } - else { - this.dy = this.dx; - } - this.axes = 'xy'; - } - else { - this.axes = interaction.resizeAxes; - - if (interaction.resizeAxes === 'x') { - this.dy = 0; - } - else if (interaction.resizeAxes === 'y') { - this.dx = 0; - } - } - } - else if (action === 'gesture') { - this.touches = [pointers[0], pointers[1]]; - - if (starting) { - this.distance = touchDistance(pointers, deltaSource); - this.box = touchBBox(pointers); - this.scale = 1; - this.ds = 0; - this.angle = touchAngle(pointers, undefined, deltaSource); - this.da = 0; - } - else if (ending || event instanceof InteractEvent) { - this.distance = interaction.prevEvent.distance; - this.box = interaction.prevEvent.box; - this.scale = interaction.prevEvent.scale; - this.ds = this.scale - 1; - this.angle = interaction.prevEvent.angle; - this.da = this.angle - interaction.gesture.startAngle; - } - else { - this.distance = touchDistance(pointers, deltaSource); - this.box = touchBBox(pointers); - this.scale = this.distance / interaction.gesture.startDistance; - this.angle = touchAngle(pointers, interaction.gesture.prevAngle, deltaSource); - - this.ds = this.scale - interaction.gesture.prevScale; - this.da = this.angle - interaction.gesture.prevAngle; - } - } - - if (starting) { - this.timeStamp = interaction.downTimes[0]; - this.dt = 0; - this.duration = 0; - this.speed = 0; - this.velocityX = 0; - this.velocityY = 0; - } - else if (phase === 'inertiastart') { - this.timeStamp = interaction.prevEvent.timeStamp; - this.dt = interaction.prevEvent.dt; - this.duration = interaction.prevEvent.duration; - this.speed = interaction.prevEvent.speed; - this.velocityX = interaction.prevEvent.velocityX; - this.velocityY = interaction.prevEvent.velocityY; - } - else { - this.timeStamp = new Date().getTime(); - this.dt = this.timeStamp - interaction.prevEvent.timeStamp; - this.duration = this.timeStamp - interaction.downTimes[0]; - - if (event instanceof InteractEvent) { - var dx = this[sourceX] - interaction.prevEvent[sourceX], - dy = this[sourceY] - interaction.prevEvent[sourceY], - dt = this.dt / 1000; - - this.speed = hypot(dx, dy) / dt; - this.velocityX = dx / dt; - this.velocityY = dy / dt; - } - // if normal move or end event, use previous user event coords - else { - // speed and velocity in pixels per second - this.speed = interaction.pointerDelta[deltaSource].speed; - this.velocityX = interaction.pointerDelta[deltaSource].vx; - this.velocityY = interaction.pointerDelta[deltaSource].vy; - } - } - - if ((ending || phase === 'inertiastart') - && interaction.prevEvent.speed > 600 && this.timeStamp - interaction.prevEvent.timeStamp < 150) { - - var angle = 180 * Math.atan2(interaction.prevEvent.velocityY, interaction.prevEvent.velocityX) / Math.PI, - overlap = 22.5; - - if (angle < 0) { - angle += 360; - } - - var left = 135 - overlap <= angle && angle < 225 + overlap, - up = 225 - overlap <= angle && angle < 315 + overlap, - - right = !left && (315 - overlap <= angle || angle < 45 + overlap), - down = !up && 45 - overlap <= angle && angle < 135 + overlap; - - this.swipe = { - up : up, - down : down, - left : left, - right: right, - angle: angle, - speed: interaction.prevEvent.speed, - velocity: { - x: interaction.prevEvent.velocityX, - y: interaction.prevEvent.velocityY - } - }; - } - } - - InteractEvent.prototype = { - preventDefault: blank, - stopImmediatePropagation: function () { - this.immediatePropagationStopped = this.propagationStopped = true; - }, - stopPropagation: function () { - this.propagationStopped = true; - } - }; - - function preventOriginalDefault () { - this.originalEvent.preventDefault(); - } - - function getActionCursor (action) { - var cursor = ''; - - if (action.name === 'drag') { - cursor = actionCursors.drag; - } - if (action.name === 'resize') { - if (action.axis) { - cursor = actionCursors[action.name + action.axis]; - } - else if (action.edges) { - var cursorKey = 'resize', - edgeNames = ['top', 'bottom', 'left', 'right']; - - for (var i = 0; i < 4; i++) { - if (action.edges[edgeNames[i]]) { - cursorKey += edgeNames[i]; - } - } - - cursor = actionCursors[cursorKey]; - } - } - - return cursor; - } - - function checkResizeEdge (name, value, page, element, interactableElement, rect, margin) { - // false, '', undefined, null - if (!value) { return false; } - - // true value, use pointer coords and element rect - if (value === true) { - // if dimensions are negative, "switch" edges - var width = isNumber(rect.width)? rect.width : rect.right - rect.left, - height = isNumber(rect.height)? rect.height : rect.bottom - rect.top; - - 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' ) { return page.x < ((width >= 0? rect.left: rect.right ) + margin); } - if (name === 'top' ) { return page.y < ((height >= 0? rect.top : rect.bottom) + 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 (!isElement(element)) { return false; } - - return isElement(value) - // the value is an element to use as a resize handle - ? value === element - // otherwise check if element matches value as selector - : matchesUpTo(element, value, interactableElement); - } - - function defaultActionChecker (pointer, interaction, element) { - var rect = this.getRect(element), - shouldResize = false, - action = null, - resizeAxes = null, - resizeEdges, - page = extend({}, interaction.curCoords.page), - options = this.options; - - if (!rect) { return null; } - - if (actionIsEnabled.resize && options.resize.enabled) { - var resizeOptions = options.resize; - - resizeEdges = { - left: false, right: false, top: false, bottom: false - }; - - // if using resize.edges - if (isObject(resizeOptions.edges)) { - for (var edge in resizeEdges) { - resizeEdges[edge] = checkResizeEdge(edge, - resizeOptions.edges[edge], - page, - interaction._eventTarget, - element, - rect, - resizeOptions.margin || margin); - } - - resizeEdges.left = resizeEdges.left && !resizeEdges.right; - resizeEdges.top = resizeEdges.top && !resizeEdges.bottom; - - shouldResize = resizeEdges.left || resizeEdges.right || resizeEdges.top || resizeEdges.bottom; - } - else { - var right = options.resize.axis !== 'y' && page.x > (rect.right - margin), - bottom = options.resize.axis !== 'x' && page.y > (rect.bottom - margin); - - shouldResize = right || bottom; - resizeAxes = (right? 'x' : '') + (bottom? 'y' : ''); - } - } - - action = shouldResize - ? 'resize' - : actionIsEnabled.drag && options.drag.enabled - ? 'drag' - : null; - - if (actionIsEnabled.gesture - && interaction.pointerIds.length >=2 - && !(interaction.dragging || interaction.resizing)) { - action = 'gesture'; - } - - if (action) { - return { - name: action, - axis: resizeAxes, - edges: resizeEdges - }; - } - - return null; - } - - // Check if action is enabled globally and the current target supports it - // If so, return the validated action. Otherwise, return null - function validateAction (action, interactable) { - if (!isObject(action)) { return null; } - - var actionName = action.name, - options = interactable.options; - - if (( (actionName === 'resize' && options.resize.enabled ) - || (actionName === 'drag' && options.drag.enabled ) - || (actionName === 'gesture' && options.gesture.enabled)) - && actionIsEnabled[actionName]) { - - if (actionName === 'resize' || actionName === 'resizeyx') { - actionName = 'resizexy'; - } - - return action; - } - return null; - } - - var listeners = {}, - interactionListeners = [ - 'dragStart', 'dragMove', 'resizeStart', 'resizeMove', 'gestureStart', 'gestureMove', - 'pointerOver', 'pointerOut', 'pointerHover', 'selectorDown', - 'pointerDown', 'pointerMove', 'pointerUp', 'pointerCancel', 'pointerEnd', - 'addPointer', 'removePointer', 'recordPointer', 'autoScrollMove' - ]; - - for (var i = 0, len = interactionListeners.length; i < len; i++) { - var name = interactionListeners[i]; - - listeners[name] = doOnInteractions(name); - } - - // bound to the interactable context when a DOM event - // listener is added to a selector interactable - function delegateListener (event, useCapture) { - var fakeEvent = {}, - delegated = delegatedEvents[event.type], - eventTarget = getActualElement(event.path - ? event.path[0] - : event.target), - element = eventTarget; - - useCapture = useCapture? true: false; - - // duplicate the event so that currentTarget can be changed - for (var prop in event) { - fakeEvent[prop] = event[prop]; - } - - fakeEvent.originalEvent = event; - fakeEvent.preventDefault = preventOriginalDefault; - - // climb up document tree looking for selector matches - while (isElement(element)) { - for (var i = 0; i < delegated.selectors.length; i++) { - var selector = delegated.selectors[i], - context = delegated.contexts[i]; - - if (matchesSelector(element, selector) - && nodeContains(context, eventTarget) - && nodeContains(context, element)) { - - var listeners = delegated.listeners[i]; - - fakeEvent.currentTarget = element; - - for (var j = 0; j < listeners.length; j++) { - if (listeners[j][1] === useCapture) { - listeners[j][0](fakeEvent); - } - } - } - } - - element = parentElement(element); - } - } - - function delegateUseCapture (event) { - return delegateListener.call(this, event, true); - } - - interactables.indexOfElement = function indexOfElement (element, context) { - context = context || document; - - for (var i = 0; i < this.length; i++) { - var interactable = this[i]; - - if ((interactable.selector === element - && (interactable._context === context)) - || (!interactable.selector && interactable._element === element)) { - - return i; - } - } - return -1; - }; - - interactables.get = function interactableGet (element, options) { - return this[this.indexOfElement(element, options && options.context)]; - }; - - interactables.forEachSelector = function (callback) { - for (var i = 0; i < this.length; i++) { - var interactable = this[i]; - - if (!interactable.selector) { - continue; - } - - var ret = callback(interactable, interactable.selector, interactable._context, i, this); - - if (ret !== undefined) { - return ret; - } - } - }; - - /*\ - * interact - [ method ] - * - * 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. - * - - element (Element | string) The HTML or SVG Element to interact with or CSS selector - = (object) An @Interactable - * - > Usage - | interact(document.getElementById('draggable')).draggable(true); - | - | var rectables = interact('rect'); - | rectables - | .gesturable(true) - | .on('gesturemove', function (event) { - | // something cool... - | }) - | .autoScroll(true); - \*/ - function interact (element, options) { - return interactables.get(element, options) || new Interactable(element, options); - } - - /*\ - * Interactable - [ property ] - ** - * Object type returned by @interact - \*/ - function Interactable (element, options) { - this._element = element; - this._iEvents = this._iEvents || {}; - - var _window; - - if (trySelector(element)) { - this.selector = element; - - var context = options && options.context; - - _window = context? getWindow(context) : window; - - if (context && (_window.Node - ? context instanceof _window.Node - : (isElement(context) || context === _window.document))) { - - this._context = context; - } - } - else { - _window = getWindow(element); - - if (isElement(element, _window)) { - - if (PointerEvent) { - events.add(this._element, pEventTypes.down, listeners.pointerDown ); - events.add(this._element, pEventTypes.move, listeners.pointerHover); - } - else { - events.add(this._element, 'mousedown' , listeners.pointerDown ); - events.add(this._element, 'mousemove' , listeners.pointerHover); - events.add(this._element, 'touchstart', listeners.pointerDown ); - events.add(this._element, 'touchmove' , listeners.pointerHover); - } - } - } - - this._doc = _window.document; - - if (!contains(documents, this._doc)) { - listenToDocument(this._doc); - } - - interactables.push(this); - - this.set(options); - } - - Interactable.prototype = { - setOnEvents: function (action, phases) { - if (action === 'drop') { - if (isFunction(phases.ondrop) ) { this.ondrop = phases.ondrop ; } - if (isFunction(phases.ondropactivate) ) { this.ondropactivate = phases.ondropactivate ; } - if (isFunction(phases.ondropdeactivate)) { this.ondropdeactivate = phases.ondropdeactivate; } - if (isFunction(phases.ondragenter) ) { this.ondragenter = phases.ondragenter ; } - if (isFunction(phases.ondragleave) ) { this.ondragleave = phases.ondragleave ; } - if (isFunction(phases.ondropmove) ) { this.ondropmove = phases.ondropmove ; } - } - else { - action = 'on' + action; - - if (isFunction(phases.onstart) ) { this[action + 'start' ] = phases.onstart ; } - if (isFunction(phases.onmove) ) { this[action + 'move' ] = phases.onmove ; } - if (isFunction(phases.onend) ) { this[action + 'end' ] = phases.onend ; } - if (isFunction(phases.oninertiastart)) { this[action + 'inertiastart' ] = phases.oninertiastart ; } - } - - return this; - }, - - /*\ - * Interactable.draggable - [ method ] - * - * Gets or sets whether drag actions can be performed on the - * Interactable - * - = (boolean) Indicates if this can be the target of drag events - | var isDraggable = interact('ul li').draggable(); - * or - - options (boolean | object) #optional true/false or An object with event listeners to be fired on drag events (object makes the Interactable draggable) - = (object) This Interactable - | 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 - | axis: 'x' || 'y' || 'xy', - | - | // 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 - | }); - \*/ - draggable: function (options) { - if (isObject(options)) { - this.options.drag.enabled = options.enabled === false? false: true; - this.setPerAction('drag', options); - this.setOnEvents('drag', options); - - if (/^x$|^y$|^xy$/.test(options.axis)) { - this.options.drag.axis = options.axis; - } - else if (options.axis === null) { - delete this.options.drag.axis; - } - - return this; - } - - if (isBool(options)) { - this.options.drag.enabled = options; - - return this; - } - - return this.options.drag; - }, - - setPerAction: function (action, options) { - // for all the default per-action options - for (var option in options) { - // if this option exists for this action - if (option in defaultOptions[action]) { - // if the option in the options arg is an object value - if (isObject(options[option])) { - // duplicate the object - this.options[action][option] = extend(this.options[action][option] || {}, options[option]); - - if (isObject(defaultOptions.perAction[option]) && 'enabled' in defaultOptions.perAction[option]) { - this.options[action][option].enabled = options[option].enabled === false? false : true; - } - } - else if (isBool(options[option]) && isObject(defaultOptions.perAction[option])) { - this.options[action][option].enabled = options[option]; - } - else if (options[option] !== undefined) { - // or if it's not undefined, do a plain assignment - this.options[action][option] = options[option]; - } - } - } - }, - - /*\ - * Interactable.dropzone - [ method ] - * - * Returns or sets whether elements can be dropped onto this - * Interactable 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. - * - * 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 - * - - options (boolean | object | null) #optional The new value to be set. - | interact('.drop').dropzone({ - | accept: '.can-drop' || document.getElementById('single-drop'), - | overlap: 'pointer' || 'center' || zeroToOne - | } - = (boolean | object) The current setting or this Interactable - \*/ - dropzone: function (options) { - if (isObject(options)) { - this.options.drop.enabled = options.enabled === false? false: true; - this.setOnEvents('drop', options); - - if (/^(pointer|center)$/.test(options.overlap)) { - this.options.drop.overlap = options.overlap; - } - else if (isNumber(options.overlap)) { - this.options.drop.overlap = Math.max(Math.min(1, options.overlap), 0); - } - if ('accept' in options) { - this.options.drop.accept = options.accept; - } - if ('checker' in options) { - this.options.drop.checker = options.checker; - } - - return this; - } - - if (isBool(options)) { - this.options.drop.enabled = options; - - return this; - } - - return this.options.drop; - }, - - dropCheck: function (dragEvent, event, draggable, draggableElement, dropElement, rect) { - var dropped = false; - - // if the dropzone has no rect (eg. display: none) - // call the custom dropChecker or just return false - if (!(rect = rect || this.getRect(dropElement))) { - return (this.options.drop.checker - ? this.options.drop.checker(dragEvent, event, dropped, this, dropElement, draggable, draggableElement) - : false); - } - - var dropOverlap = this.options.drop.overlap; - - if (dropOverlap === 'pointer') { - var page = getPageXY(dragEvent), - origin = getOriginXY(draggable, draggableElement), - horizontal, - vertical; - - page.x += origin.x; - page.y += origin.y; - - horizontal = (page.x > rect.left) && (page.x < rect.right); - vertical = (page.y > rect.top ) && (page.y < rect.bottom); - - dropped = horizontal && vertical; - } - - var dragRect = draggable.getRect(draggableElement); - - if (dropOverlap === 'center') { - var cx = dragRect.left + dragRect.width / 2, - cy = dragRect.top + dragRect.height / 2; - - dropped = cx >= rect.left && cx <= rect.right && cy >= rect.top && cy <= rect.bottom; - } - - if (isNumber(dropOverlap)) { - var 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 ))), - overlapRatio = overlapArea / (dragRect.width * dragRect.height); - - dropped = overlapRatio >= dropOverlap; - } - - if (this.options.drop.checker) { - dropped = this.options.drop.checker(dragEvent, event, dropped, this, dropElement, draggable, draggableElement); - } - - return dropped; - }, - - /*\ - * Interactable.dropChecker - [ method ] - * - * DEPRECATED. Use interactable.dropzone({ checker: function... }) instead. - * - * Gets or sets the function used to check if a dragged element is - * over this Interactable. - * - - checker (function) #optional The function that will be called when checking for a drop - = (Function | Interactable) The checker function or this Interactable - * - * The checker function takes the following arguments: - * - - dragEvent (InteractEvent) The related dragmove or dragend event - - event (TouchEvent | PointerEvent | MouseEvent) The user move/up/end Event related to the dragEvent - - dropped (boolean) The value from the default drop checker - - dropzone (Interactable) The dropzone interactable - - dropElement (Element) The dropzone element - - draggable (Interactable) The Interactable being dragged - - draggableElement (Element) The actual element that's being dragged - * - > Usage: - | 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'); - | } - \*/ - dropChecker: function (checker) { - if (isFunction(checker)) { - this.options.drop.checker = checker; - - return this; - } - if (checker === null) { - delete this.options.getRect; - - return this; - } - - return this.options.drop.checker; - }, - - /*\ - * Interactable.accept - [ method ] - * - * Deprecated. add an `accept` property to the options object passed to - * @Interactable.dropzone instead. - * - * Gets or sets the Element or CSS selector match that this - * Interactable accepts if it is a dropzone. - * - - newValue (Element | string | null) #optional - * If it is an Element, then only that element can be dropped into this dropzone. - * If it is a string, the element being dragged must match it as a selector. - * If it is null, the accept options is cleared - it accepts any element. - * - = (string | Element | null | Interactable) The current accept option if given `undefined` or this Interactable - \*/ - accept: function (newValue) { - if (isElement(newValue)) { - this.options.drop.accept = newValue; - - return this; - } - - // test if it is a valid CSS selector - if (trySelector(newValue)) { - this.options.drop.accept = newValue; - - return this; - } - - if (newValue === null) { - delete this.options.drop.accept; - - return this; - } - - return this.options.drop.accept; - }, - - /*\ - * Interactable.resizable - [ method ] - * - * Gets or sets whether resize actions can be performed on the - * Interactable - * - = (boolean) Indicates if this can be the target of resize elements - | var isResizeable = interact('input[type=text]').resizable(); - * or - - options (boolean | object) #optional true/false or An object with event listeners to be fired on resize events (object makes the Interactable resizable) - = (object) This Interactable - | 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 @Interactable.draggable example - | max: Infinity, - | maxPerElement: 1, - | }); - \*/ - resizable: function (options) { - if (isObject(options)) { - this.options.resize.enabled = options.enabled === false? false: true; - this.setPerAction('resize', options); - this.setOnEvents('resize', options); - - if (/^x$|^y$|^xy$/.test(options.axis)) { - this.options.resize.axis = options.axis; - } - else if (options.axis === null) { - this.options.resize.axis = defaultOptions.resize.axis; - } - - if (isBool(options.preserveAspectRatio)) { - this.options.resize.preserveAspectRatio = options.preserveAspectRatio; - } - else if (isBool(options.square)) { - this.options.resize.square = options.square; - } - - return this; - } - if (isBool(options)) { - this.options.resize.enabled = options; - - return this; - } - return this.options.resize; - }, - - /*\ - * Interactable.squareResize - [ method ] - * - * Deprecated. Add a `square: true || false` property to @Interactable.resizable instead - * - * Gets or sets whether resizing is forced 1:1 aspect - * - = (boolean) Current setting - * - * or - * - - newValue (boolean) #optional - = (object) this Interactable - \*/ - squareResize: function (newValue) { - if (isBool(newValue)) { - this.options.resize.square = newValue; - - return this; - } - - if (newValue === null) { - delete this.options.resize.square; - - return this; - } - - return this.options.resize.square; - }, - - /*\ - * Interactable.gesturable - [ method ] - * - * Gets or sets whether multitouch gestures can be performed on the - * Interactable's element - * - = (boolean) Indicates if this can be the target of gesture events - | var isGestureable = interact(element).gesturable(); - * or - - options (boolean | object) #optional true/false or An object with event listeners to be fired on gesture events (makes the Interactable gesturable) - = (object) this Interactable - | interact(element).gesturable({ - | onstart: function (event) {}, - | onmove : function (event) {}, - | onend : function (event) {}, - | - | // limit multiple gestures. - | // See the explanation in @Interactable.draggable example - | max: Infinity, - | maxPerElement: 1, - | }); - \*/ - gesturable: function (options) { - if (isObject(options)) { - this.options.gesture.enabled = options.enabled === false? false: true; - this.setPerAction('gesture', options); - this.setOnEvents('gesture', options); - - return this; - } - - if (isBool(options)) { - this.options.gesture.enabled = options; - - return this; - } - - return this.options.gesture; - }, - - /*\ - * Interactable.autoScroll - [ method ] - ** - * Deprecated. Add an `autoscroll` property to the options object - * passed to @Interactable.draggable or @Interactable.resizable instead. - * - * Returns or sets whether dragging and resizing near the edges of the - * window/container trigger autoScroll for this Interactable - * - = (object) Object with autoScroll properties - * - * or - * - - options (object | boolean) #optional - * options can be: - * - an object with margin, distance and interval properties, - * - true or false to enable or disable autoScroll or - = (Interactable) this Interactable - \*/ - autoScroll: function (options) { - if (isObject(options)) { - options = extend({ actions: ['drag', 'resize']}, options); - } - else if (isBool(options)) { - options = { actions: ['drag', 'resize'], enabled: options }; - } - - return this.setOptions('autoScroll', options); - }, - - /*\ - * Interactable.snap - [ method ] - ** - * Deprecated. Add a `snap` property to the options object passed - * to @Interactable.draggable or @Interactable.resizable instead. - * - * Returns or sets if and how action coordinates are snapped. By - * default, snapping is relative to the pointer coordinates. You can - * change this by setting the - * [`elementOrigin`](https://github.com/taye/interact.js/pull/72). - ** - = (boolean | object) `false` if snap is disabled; object with snap properties if snap is enabled - ** - * or - ** - - options (object | boolean | null) #optional - = (Interactable) this Interactable - > Usage - | interact(document.querySelector('#thing')).snap({ - | targets: [ - | // snap to this specific point - | { - | x: 100, - | y: 100, - | range: 25 - | }, - | // give this function the x and y page coords and snap to the object returned - | function (x, y) { - | return { - | x: x, - | y: (75 + 50 * Math.sin(x * 0.04)), - | range: 40 - | }; - | }, - | // create a function that snaps to a grid - | interact.createSnapGrid({ - | x: 50, - | y: 50, - | range: 10, // optional - | offset: { x: 5, y: 10 } // optional - | }) - | ], - | // do not snap during normal movement. - | // Instead, trigger only one snapped move event - | // immediately before the end event. - | endOnly: true, - | - | 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 (options) { - var ret = this.setOptions('snap', options); - - if (ret === this) { return this; } - - return ret.drag; - }, - - setOptions: function (option, options) { - var actions = options && isArray(options.actions) - ? options.actions - : ['drag']; - - var i; - - if (isObject(options) || isBool(options)) { - for (i = 0; i < actions.length; i++) { - var action = /resize/.test(actions[i])? 'resize' : actions[i]; - - if (!isObject(this.options[action])) { continue; } - - var thisOption = this.options[action][option]; - - if (isObject(options)) { - extend(thisOption, options); - thisOption.enabled = options.enabled === false? false: true; - - if (option === 'snap') { - if (thisOption.mode === 'grid') { - thisOption.targets = [ - interact.createSnapGrid(extend({ - offset: thisOption.gridOffset || { x: 0, y: 0 } - }, thisOption.grid || {})) - ]; - } - else if (thisOption.mode === 'anchor') { - thisOption.targets = thisOption.anchors; - } - else if (thisOption.mode === 'path') { - thisOption.targets = thisOption.paths; - } - - if ('elementOrigin' in options) { - thisOption.relativePoints = [options.elementOrigin]; - } - } - } - else if (isBool(options)) { - thisOption.enabled = options; - } - } - - return this; - } - - var ret = {}, - allActions = ['drag', 'resize', 'gesture']; - - for (i = 0; i < allActions.length; i++) { - if (option in defaultOptions[allActions[i]]) { - ret[allActions[i]] = this.options[allActions[i]][option]; - } - } - - return ret; - }, - - - /*\ - * Interactable.inertia - [ method ] - ** - * Deprecated. Add an `inertia` property to the options object passed - * to @Interactable.draggable or @Interactable.resizable instead. - * - * Returns or sets if and how events continue to run after the pointer is released - ** - = (boolean | object) `false` if inertia is disabled; `object` with inertia properties if inertia is enabled - ** - * or - ** - - options (object | boolean | null) #optional - = (Interactable) this Interactable - > Usage - | // enable and use default settings - | interact(element).inertia(true); - | - | // enable and use custom settings - | interact(element).inertia({ - | // value greater than 0 - | // high values slow the object down more quickly - | resistance : 16, - | - | // the minimum launch speed (pixels per second) that results in inertia start - | minSpeed : 200, - | - | // inertia will stop when the object slows down to this speed - | endSpeed : 20, - | - | // boolean; should actions be resumed when the pointer goes down during inertia - | allowResume : true, - | - | // boolean; should the jump when resuming from inertia be ignored in event.dx/dy - | zeroResumeDelta: false, - | - | // if snap/restrict are set to be endOnly and inertia is enabled, releasing - | // the pointer without triggering inertia will animate from the release - | // point to the snaped/restricted point in the given amount of time (ms) - | smoothEndDuration: 300, - | - | // an array of action types that can have inertia (no gesture) - | actions : ['drag', 'resize'] - | }); - | - | // reset custom settings and use all defaults - | interact(element).inertia(null); - \*/ - inertia: function (options) { - var ret = this.setOptions('inertia', options); - - if (ret === this) { return this; } - - return ret.drag; - }, - - getAction: function (pointer, event, interaction, element) { - var action = this.defaultActionChecker(pointer, interaction, element); - - if (this.options.actionChecker) { - return this.options.actionChecker(pointer, event, action, this, element, interaction); - } - - return action; - }, - - defaultActionChecker: defaultActionChecker, - - /*\ - * Interactable.actionChecker - [ method ] - * - * Gets or sets the function used to check action to be performed on - * pointerDown - * - - checker (function | null) #optional 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. - = (Function | Interactable) The checker function or this Interactable - * - | 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; - | }); - \*/ - actionChecker: function (checker) { - if (isFunction(checker)) { - this.options.actionChecker = checker; - - return this; - } - - if (checker === null) { - delete this.options.actionChecker; - - return this; - } - - return this.options.actionChecker; - }, - - /*\ - * Interactable.getRect - [ method ] - * - * The default function to get an Interactables bounding rect. Can be - * overridden using @Interactable.rectChecker. - * - - element (Element) #optional The element to measure. - = (object) The object's bounding rectangle. - o { - o top : 0, - o left : 0, - o bottom: 0, - o right : 0, - o width : 0, - o height: 0 - o } - \*/ - getRect: function rectCheck (element) { - element = element || this._element; - - if (this.selector && !(isElement(element))) { - element = this._context.querySelector(this.selector); - } - - return getElementRect(element); - }, - - /*\ - * Interactable.rectChecker - [ method ] - * - * Returns or sets the function used to calculate the interactable's - * element's rectangle - * - - checker (function) #optional A function which returns this Interactable's bounding rectangle. See @Interactable.getRect - = (function | object) The checker function or this Interactable - \*/ - rectChecker: function (checker) { - if (isFunction(checker)) { - this.getRect = checker; - - return this; - } - - if (checker === null) { - delete this.options.getRect; - - return this; - } - - return this.getRect; - }, - - /*\ - * Interactable.styleCursor - [ method ] - * - * Returns or sets whether the action that would be performed when the - * mouse on the element are checked on `mousemove` so that the cursor - * may be styled appropriately - * - - newValue (boolean) #optional - = (boolean | Interactable) The current setting or this Interactable - \*/ - styleCursor: function (newValue) { - if (isBool(newValue)) { - this.options.styleCursor = newValue; - - return this; - } - - if (newValue === null) { - delete this.options.styleCursor; - - return this; - } - - return this.options.styleCursor; - }, - - /*\ - * Interactable.preventDefault - [ method ] - * - * 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 - * - - newValue (string) #optional `true`, `false` or `'auto'` - = (string | Interactable) The current setting or this Interactable - \*/ - preventDefault: function (newValue) { - if (/^(always|never|auto)$/.test(newValue)) { - this.options.preventDefault = newValue; - return this; - } - - if (isBool(newValue)) { - this.options.preventDefault = newValue? 'always' : 'never'; - return this; - } - - return this.options.preventDefault; - }, - - /*\ - * Interactable.origin - [ method ] - * - * Gets or sets the origin of the Interactable's element. The x and y - * of the origin will be subtracted from action event coordinates. - * - - origin (object | string) #optional An object eg. { x: 0, y: 0 } or string 'parent', 'self' or any CSS selector - * OR - - origin (Element) #optional An HTML or SVG Element whose rect will be used - ** - = (object) The current origin or this Interactable - \*/ - origin: function (newValue) { - if (trySelector(newValue)) { - this.options.origin = newValue; - return this; - } - else if (isObject(newValue)) { - this.options.origin = newValue; - return this; - } - - return this.options.origin; - }, - - /*\ - * Interactable.deltaSource - [ method ] - * - * Returns or sets the mouse coordinate types used to calculate the - * movement of the pointer. - * - - newValue (string) #optional Use 'client' if you will be scrolling while interacting; Use 'page' if you want autoScroll to work - = (string | object) The current deltaSource or this Interactable - \*/ - deltaSource: function (newValue) { - if (newValue === 'page' || newValue === 'client') { - this.options.deltaSource = newValue; - - return this; - } - - return this.options.deltaSource; - }, - - /*\ - * Interactable.restrict - [ method ] - ** - * Deprecated. Add a `restrict` property to the options object passed to - * @Interactable.draggable, @Interactable.resizable or @Interactable.gesturable instead. - * - * Returns or sets the rectangles within which actions on this - * interactable (after snap calculations) are restricted. By default, - * restricting is relative to the pointer coordinates. You can change - * this by setting the - * [`elementRect`](https://github.com/taye/interact.js/pull/72). - ** - - options (object) #optional an object with keys drag, resize, and/or gesture whose values are rects, Elements, CSS selectors, or 'parent' or 'self' - = (object) The current restrictions object or this Interactable - ** - | interact(element).restrict({ - | // the rect will be `interact.getElementRect(element.parentNode)` - | drag: element.parentNode, - | - | // x and y are relative to the the interactable's origin - | resize: { x: 100, y: 100, width: 200, height: 200 } - | }) - | - | interact('.draggable').restrict({ - | // the rect will be the selected element's parent - | drag: 'parent', - | - | // do not restrict during normal movement. - | // Instead, trigger only one restricted move event - | // immediately before the end event. - | endOnly: true, - | - | // https://github.com/taye/interact.js/pull/72#issue-41813493 - | elementRect: { top: 0, left: 0, bottom: 1, right: 1 } - | }); - \*/ - restrict: function (options) { - if (!isObject(options)) { - return this.setOptions('restrict', options); - } - - var actions = ['drag', 'resize', 'gesture'], - ret; - - for (var i = 0; i < actions.length; i++) { - var action = actions[i]; - - if (action in options) { - var perAction = extend({ - actions: [action], - restriction: options[action] - }, options); - - ret = this.setOptions('restrict', perAction); - } - } - - return ret; - }, - - /*\ - * Interactable.context - [ method ] - * - * Gets the selector context Node of the Interactable. The default is `window.document`. - * - = (Node) The context Node of this Interactable - ** - \*/ - context: function () { - return this._context; - }, - - _context: document, - - /*\ - * Interactable.ignoreFrom - [ method ] - * - * 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. - * - - newValue (string | Element | null) #optional a CSS selector string, an Element or `null` to not ignore any elements - = (string | Element | object) The current ignoreFrom value or this Interactable - ** - | interact(element, { ignoreFrom: document.getElementById('no-action') }); - | // or - | interact(element).ignoreFrom('input, textarea, a'); - \*/ - ignoreFrom: function (newValue) { - if (trySelector(newValue)) { // CSS selector to match event.target - this.options.ignoreFrom = newValue; - return this; - } - - if (isElement(newValue)) { // specific element - this.options.ignoreFrom = newValue; - return this; - } - - return this.options.ignoreFrom; - }, - - /*\ - * Interactable.allowFrom - [ method ] - * - * 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. - * - - newValue (string | Element | null) #optional a CSS selector string, an Element or `null` to allow from any element - = (string | Element | object) The current allowFrom value or this Interactable - ** - | interact(element, { allowFrom: document.getElementById('drag-handle') }); - | // or - | interact(element).allowFrom('.handle'); - \*/ - allowFrom: function (newValue) { - if (trySelector(newValue)) { // CSS selector to match event.target - this.options.allowFrom = newValue; - return this; - } - - if (isElement(newValue)) { // specific element - this.options.allowFrom = newValue; - return this; - } - - return this.options.allowFrom; - }, - - /*\ - * Interactable.element - [ method ] - * - * If this is not a selector Interactable, it returns the element this - * interactable represents - * - = (Element) HTML / SVG Element - \*/ - element: function () { - return this._element; - }, - - /*\ - * Interactable.fire - [ method ] - * - * Calls listeners for the given InteractEvent type bound globally - * and directly to this Interactable - * - - iEvent (InteractEvent) The InteractEvent object to be fired on this Interactable - = (Interactable) this Interactable - \*/ - fire: function (iEvent) { - if (!(iEvent && iEvent.type) || !contains(eventTypes, iEvent.type)) { - return this; - } - - var listeners, - i, - len, - onEvent = 'on' + iEvent.type, - funcName = ''; - - // Interactable#on() listeners - if (iEvent.type in this._iEvents) { - listeners = this._iEvents[iEvent.type]; - - for (i = 0, len = listeners.length; i < len && !iEvent.immediatePropagationStopped; i++) { - funcName = listeners[i].name; - listeners[i](iEvent); - } - } - - // interactable.onevent listener - if (isFunction(this[onEvent])) { - funcName = this[onEvent].name; - this[onEvent](iEvent); - } - - // interact.on() listeners - if (iEvent.type in globalEvents && (listeners = globalEvents[iEvent.type])) { - - for (i = 0, len = listeners.length; i < len && !iEvent.immediatePropagationStopped; i++) { - funcName = listeners[i].name; - listeners[i](iEvent); - } - } - - return this; - }, - - /*\ - * Interactable.on - [ method ] - * - * Binds a listener for an InteractEvent or DOM event. - * - - eventType (string | array | object) The types of events to listen for - - listener (function) The function to be called on the given event(s) - - useCapture (boolean) #optional useCapture flag for addEventListener - = (object) This Interactable - \*/ - on: function (eventType, listener, useCapture) { - var i; - - if (isString(eventType) && eventType.search(' ') !== -1) { - eventType = eventType.trim().split(/ +/); - } - - if (isArray(eventType)) { - for (i = 0; i < eventType.length; i++) { - this.on(eventType[i], listener, useCapture); - } - - return this; - } - - if (isObject(eventType)) { - for (var prop in eventType) { - this.on(prop, eventType[prop], listener); - } - - return this; - } - - if (eventType === 'wheel') { - eventType = wheelEvent; - } - - // convert to boolean - useCapture = useCapture? true: false; - - if (contains(eventTypes, eventType)) { - // if this type of event was never bound to this Interactable - if (!(eventType in this._iEvents)) { - this._iEvents[eventType] = [listener]; - } - else { - this._iEvents[eventType].push(listener); - } - } - // delegated event for selector - else if (this.selector) { - if (!delegatedEvents[eventType]) { - delegatedEvents[eventType] = { - selectors: [], - contexts : [], - listeners: [] - }; - - // add delegate listener functions - for (i = 0; i < documents.length; i++) { - events.add(documents[i], eventType, delegateListener); - events.add(documents[i], eventType, delegateUseCapture, true); - } - } - - var delegated = delegatedEvents[eventType], - index; - - for (index = delegated.selectors.length - 1; index >= 0; index--) { - if (delegated.selectors[index] === this.selector - && delegated.contexts[index] === this._context) { - break; - } - } - - if (index === -1) { - index = delegated.selectors.length; - - delegated.selectors.push(this.selector); - delegated.contexts .push(this._context); - delegated.listeners.push([]); - } - - // keep listener and useCapture flag - delegated.listeners[index].push([listener, useCapture]); - } - else { - events.add(this._element, eventType, listener, useCapture); - } - - return this; - }, - - /*\ - * Interactable.off - [ method ] - * - * Removes an InteractEvent or DOM event listener - * - - eventType (string | array | object) The types of events that were listened for - - listener (function) The listener function to be removed - - useCapture (boolean) #optional useCapture flag for removeEventListener - = (object) This Interactable - \*/ - off: function (eventType, listener, useCapture) { - var i; - - if (isString(eventType) && eventType.search(' ') !== -1) { - eventType = eventType.trim().split(/ +/); - } - - if (isArray(eventType)) { - for (i = 0; i < eventType.length; i++) { - this.off(eventType[i], listener, useCapture); - } - - return this; - } - - if (isObject(eventType)) { - for (var prop in eventType) { - this.off(prop, eventType[prop], listener); - } - - return this; - } - - var eventList, - index = -1; - - // convert to boolean - useCapture = useCapture? true: false; - - if (eventType === 'wheel') { - eventType = wheelEvent; - } - - // if it is an action event type - if (contains(eventTypes, eventType)) { - eventList = this._iEvents[eventType]; - - if (eventList && (index = indexOf(eventList, listener)) !== -1) { - this._iEvents[eventType].splice(index, 1); - } - } - // delegated event - else if (this.selector) { - var delegated = delegatedEvents[eventType], - matchFound = false; - - if (!delegated) { return this; } - - // count from last index of delegated to 0 - for (index = delegated.selectors.length - 1; index >= 0; index--) { - // look for matching selector and context Node - if (delegated.selectors[index] === this.selector - && delegated.contexts[index] === this._context) { - - var listeners = delegated.listeners[index]; - - // each item of the listeners array is an array: [function, useCaptureFlag] - for (i = listeners.length - 1; i >= 0; i--) { - var fn = listeners[i][0], - useCap = listeners[i][1]; - - // check if the listener functions and useCapture flags match - if (fn === listener && useCap === useCapture) { - // remove the listener from the array of listeners - listeners.splice(i, 1); - - // if all listeners for this interactable have been removed - // remove the interactable from the delegated arrays - if (!listeners.length) { - delegated.selectors.splice(index, 1); - delegated.contexts .splice(index, 1); - delegated.listeners.splice(index, 1); - - // remove delegate function from context - events.remove(this._context, eventType, delegateListener); - events.remove(this._context, eventType, delegateUseCapture, true); - - // remove the arrays if they are empty - if (!delegated.selectors.length) { - delegatedEvents[eventType] = null; - } - } - - // only remove one listener - matchFound = true; - break; - } - } - - if (matchFound) { break; } - } - } - } - // remove listener from this Interatable's element - else { - events.remove(this._element, eventType, listener, useCapture); - } - - return this; - }, - - /*\ - * Interactable.set - [ method ] - * - * Reset the options of this Interactable - - options (object) The new settings to apply - = (object) This Interactable - \*/ - set: function (options) { - if (!isObject(options)) { - options = {}; - } - - this.options = extend({}, defaultOptions.base); - - var i, - actions = ['drag', 'drop', 'resize', 'gesture'], - methods = ['draggable', 'dropzone', 'resizable', 'gesturable'], - perActions = extend(extend({}, defaultOptions.perAction), options[action] || {}); - - for (i = 0; i < actions.length; i++) { - var action = actions[i]; - - this.options[action] = extend({}, defaultOptions[action]); - - this.setPerAction(action, perActions); - - this[methods[i]](options[action]); - } - - var settings = [ - 'accept', 'actionChecker', 'allowFrom', 'deltaSource', - 'dropChecker', 'ignoreFrom', 'origin', 'preventDefault', - 'rectChecker', 'styleCursor' - ]; - - for (i = 0, len = settings.length; i < len; i++) { - var setting = settings[i]; - - this.options[setting] = defaultOptions.base[setting]; - - if (setting in options) { - this[setting](options[setting]); - } - } - - return this; - }, - - /*\ - * Interactable.unset - [ method ] - * - * Remove this interactable from the list of interactables and remove - * it's drag, drop, resize and gesture capabilities - * - = (object) @interact - \*/ - unset: function () { - events.remove(this._element, 'all'); - - if (!isString(this.selector)) { - events.remove(this, 'all'); - if (this.options.styleCursor) { - this._element.style.cursor = ''; - } - } - else { - // remove delegated events - for (var type in delegatedEvents) { - var delegated = delegatedEvents[type]; - - for (var i = 0; i < delegated.selectors.length; i++) { - if (delegated.selectors[i] === this.selector - && delegated.contexts[i] === this._context) { - - delegated.selectors.splice(i, 1); - delegated.contexts .splice(i, 1); - delegated.listeners.splice(i, 1); - - // remove the arrays if they are empty - if (!delegated.selectors.length) { - delegatedEvents[type] = null; - } - } - - events.remove(this._context, type, delegateListener); - events.remove(this._context, type, delegateUseCapture, true); - - break; - } - } - } - - this.dropzone(false); - - interactables.splice(indexOf(interactables, this), 1); - - return interact; - } - }; - - function warnOnce (method, message) { - var warned = false; - - return function () { - if (!warned) { - window.console.warn(message); - warned = true; - } - - return method.apply(this, arguments); - }; - } - - Interactable.prototype.snap = warnOnce(Interactable.prototype.snap, - 'Interactable#snap is deprecated. See the new documentation for snapping at http://interactjs.io/docs/snapping'); - Interactable.prototype.restrict = warnOnce(Interactable.prototype.restrict, - 'Interactable#restrict is deprecated. See the new documentation for resticting at http://interactjs.io/docs/restriction'); - Interactable.prototype.inertia = warnOnce(Interactable.prototype.inertia, - 'Interactable#inertia is deprecated. See the new documentation for inertia at http://interactjs.io/docs/inertia'); - Interactable.prototype.autoScroll = warnOnce(Interactable.prototype.autoScroll, - 'Interactable#autoScroll is deprecated. See the new documentation for autoScroll at http://interactjs.io/docs/#autoscroll'); - Interactable.prototype.squareResize = warnOnce(Interactable.prototype.squareResize, - 'Interactable#squareResize is deprecated. See http://interactjs.io/docs/#resize-square'); - - Interactable.prototype.accept = warnOnce(Interactable.prototype.accept, - 'Interactable#accept is deprecated. use Interactable#dropzone({ accept: target }) instead'); - Interactable.prototype.dropChecker = warnOnce(Interactable.prototype.dropChecker, - 'Interactable#dropChecker is deprecated. use Interactable#dropzone({ dropChecker: checkerFunction }) instead'); - Interactable.prototype.context = warnOnce(Interactable.prototype.context, - 'Interactable#context as a method is deprecated. It will soon be a DOM Node instead'); - - /*\ - * interact.isSet - [ method ] - * - * Check if an element has been set - - element (Element) The Element being searched for - = (boolean) Indicates if the element or CSS selector was previously passed to interact - \*/ - interact.isSet = function(element, options) { - return interactables.indexOfElement(element, options && options.context) !== -1; - }; - - /*\ - * interact.on - [ method ] - * - * Adds a global listener for an InteractEvent or adds a DOM event to - * `document` - * - - type (string | array | object) The types of events to listen for - - listener (function) The function to be called on the given event(s) - - useCapture (boolean) #optional useCapture flag for addEventListener - = (object) interact - \*/ - interact.on = function (type, listener, useCapture) { - if (isString(type) && type.search(' ') !== -1) { - type = type.trim().split(/ +/); - } - - if (isArray(type)) { - for (var i = 0; i < type.length; i++) { - interact.on(type[i], listener, useCapture); - } - - return interact; - } - - if (isObject(type)) { - for (var prop in type) { - interact.on(prop, type[prop], listener); - } - - return interact; - } - - // if it is an InteractEvent type, add listener to globalEvents - if (contains(eventTypes, type)) { - // if this type of event was never bound - if (!globalEvents[type]) { - globalEvents[type] = [listener]; - } - else { - globalEvents[type].push(listener); - } - } - // If non InteractEvent type, addEventListener to document - else { - events.add(document, type, listener, useCapture); - } - - return interact; - }; - - /*\ - * interact.off - [ method ] - * - * Removes a global InteractEvent listener or DOM event from `document` - * - - type (string | array | object) The types of events that were listened for - - listener (function) The listener function to be removed - - useCapture (boolean) #optional useCapture flag for removeEventListener - = (object) interact - \*/ - interact.off = function (type, listener, useCapture) { - if (isString(type) && type.search(' ') !== -1) { - type = type.trim().split(/ +/); - } - - if (isArray(type)) { - for (var i = 0; i < type.length; i++) { - interact.off(type[i], listener, useCapture); - } - - return interact; - } - - if (isObject(type)) { - for (var prop in type) { - interact.off(prop, type[prop], listener); - } - - return interact; - } - - if (!contains(eventTypes, type)) { - events.remove(document, type, listener, useCapture); - } - else { - var index; - - if (type in globalEvents - && (index = indexOf(globalEvents[type], listener)) !== -1) { - globalEvents[type].splice(index, 1); - } - } - - return interact; - }; - - /*\ - * interact.enableDragging - [ method ] - * - * Deprecated. - * - * Returns or sets whether dragging is enabled for any Interactables - * - - newValue (boolean) #optional `true` to allow the action; `false` to disable action for all Interactables - = (boolean | object) The current setting or interact - \*/ - interact.enableDragging = warnOnce(function (newValue) { - if (newValue !== null && newValue !== undefined) { - actionIsEnabled.drag = newValue; - - return interact; - } - return actionIsEnabled.drag; - }, 'interact.enableDragging is deprecated and will soon be removed.'); - - /*\ - * interact.enableResizing - [ method ] - * - * Deprecated. - * - * Returns or sets whether resizing is enabled for any Interactables - * - - newValue (boolean) #optional `true` to allow the action; `false` to disable action for all Interactables - = (boolean | object) The current setting or interact - \*/ - interact.enableResizing = warnOnce(function (newValue) { - if (newValue !== null && newValue !== undefined) { - actionIsEnabled.resize = newValue; - - return interact; - } - return actionIsEnabled.resize; - }, 'interact.enableResizing is deprecated and will soon be removed.'); - - /*\ - * interact.enableGesturing - [ method ] - * - * Deprecated. - * - * Returns or sets whether gesturing is enabled for any Interactables - * - - newValue (boolean) #optional `true` to allow the action; `false` to disable action for all Interactables - = (boolean | object) The current setting or interact - \*/ - interact.enableGesturing = warnOnce(function (newValue) { - if (newValue !== null && newValue !== undefined) { - actionIsEnabled.gesture = newValue; - - return interact; - } - return actionIsEnabled.gesture; - }, 'interact.enableGesturing is deprecated and will soon be removed.'); - - interact.eventTypes = eventTypes; - - /*\ - * interact.debug - [ method ] - * - * Returns debugging data - = (object) An object with properties that outline the current state and expose internal functions and variables - \*/ - interact.debug = function () { - var interaction = interactions[0] || new Interaction(); - - return { - interactions : interactions, - target : interaction.target, - dragging : interaction.dragging, - resizing : interaction.resizing, - gesturing : interaction.gesturing, - prepared : interaction.prepared, - matches : interaction.matches, - matchElements : interaction.matchElements, - - prevCoords : interaction.prevCoords, - startCoords : interaction.startCoords, - - pointerIds : interaction.pointerIds, - pointers : interaction.pointers, - addPointer : listeners.addPointer, - removePointer : listeners.removePointer, - recordPointer : listeners.recordPointer, - - snap : interaction.snapStatus, - restrict : interaction.restrictStatus, - inertia : interaction.inertiaStatus, - - downTime : interaction.downTimes[0], - downEvent : interaction.downEvent, - downPointer : interaction.downPointer, - prevEvent : interaction.prevEvent, - - Interactable : Interactable, - interactables : interactables, - pointerIsDown : interaction.pointerIsDown, - defaultOptions : defaultOptions, - defaultActionChecker : defaultActionChecker, - - actionCursors : actionCursors, - dragMove : listeners.dragMove, - resizeMove : listeners.resizeMove, - gestureMove : listeners.gestureMove, - pointerUp : listeners.pointerUp, - pointerDown : listeners.pointerDown, - pointerMove : listeners.pointerMove, - pointerHover : listeners.pointerHover, - - eventTypes : eventTypes, - - events : events, - globalEvents : globalEvents, - delegatedEvents : delegatedEvents, - - prefixedPropREs : prefixedPropREs - }; - }; - - // expose the functions used to calculate multi-touch properties - interact.getPointerAverage = pointerAverage; - interact.getTouchBBox = touchBBox; - interact.getTouchDistance = touchDistance; - interact.getTouchAngle = touchAngle; - - interact.getElementRect = getElementRect; - interact.getElementClientRect = getElementClientRect; - interact.matchesSelector = matchesSelector; - interact.closest = closest; - - /*\ - * interact.margin - [ method ] - * - * Deprecated. Use `interact(target).resizable({ margin: number });` instead. - * Returns or sets the margin for autocheck resizing used in - * @Interactable.getAction. That is the distance from the bottom and right - * edges of an element clicking in which will start resizing - * - - newValue (number) #optional - = (number | interact) The current margin value or interact - \*/ - interact.margin = warnOnce(function (newvalue) { - if (isNumber(newvalue)) { - margin = newvalue; - - return interact; - } - return margin; - }, - 'interact.margin is deprecated. Use interact(target).resizable({ margin: number }); instead.') ; - - /*\ - * interact.supportsTouch - [ method ] - * - = (boolean) Whether or not the browser supports touch input - \*/ - interact.supportsTouch = function () { - return supportsTouch; - }; - - /*\ - * interact.supportsPointerEvent - [ method ] - * - = (boolean) Whether or not the browser supports PointerEvents - \*/ - interact.supportsPointerEvent = function () { - return supportsPointerEvent; - }; - - /*\ - * interact.stop - [ method ] - * - * Cancels all interactions (end events are not fired) - * - - event (Event) An event on which to call preventDefault() - = (object) interact - \*/ - interact.stop = function (event) { - for (var i = interactions.length - 1; i >= 0; i--) { - interactions[i].stop(event); - } - - return interact; - }; - - /*\ - * interact.dynamicDrop - [ method ] - * - * Returns or sets whether the dimensions of dropzone elements are - * calculated on every dragmove or only on dragstart for the default - * dropChecker - * - - newValue (boolean) #optional True to check on each move. False to check only before start - = (boolean | interact) The current setting or interact - \*/ - interact.dynamicDrop = function (newValue) { - if (isBool(newValue)) { - //if (dragging && dynamicDrop !== newValue && !newValue) { - //calcRects(dropzones); - //} - - dynamicDrop = newValue; - - return interact; - } - return dynamicDrop; - }; - - /*\ - * interact.pointerMoveTolerance - [ method ] - * Returns or sets the distance the pointer must be moved before an action - * sequence occurs. This also affects tolerance for tap events. - * - - newValue (number) #optional The movement from the start position must be greater than this value - = (number | Interactable) The current setting or interact - \*/ - interact.pointerMoveTolerance = function (newValue) { - if (isNumber(newValue)) { - pointerMoveTolerance = newValue; - - return this; - } - - return pointerMoveTolerance; - }; - - /*\ - * interact.maxInteractions - [ method ] - ** - * 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. - ** - - newValue (number) #optional Any number. newValue <= 0 means no interactions. - \*/ - interact.maxInteractions = function (newValue) { - if (isNumber(newValue)) { - maxInteractions = newValue; - - return this; - } - - return maxInteractions; - }; - - interact.createSnapGrid = function (grid) { - return function (x, y) { - var offsetX = 0, - offsetY = 0; - - if (isObject(grid.offset)) { - offsetX = grid.offset.x; - offsetY = grid.offset.y; - } - - var gridx = Math.round((x - offsetX) / grid.x), - gridy = Math.round((y - offsetY) / grid.y), - - newX = gridx * grid.x + offsetX, - newY = gridy * grid.y + offsetY; - - return { - x: newX, - y: newY, - range: grid.range - }; - }; - }; - - function endAllInteractions (event) { - for (var i = 0; i < interactions.length; i++) { - interactions[i].pointerEnd(event, event); - } - } - - function listenToDocument (doc) { - if (contains(documents, doc)) { return; } - - var win = doc.defaultView || doc.parentWindow; - - // add delegate event listener - for (var eventType in delegatedEvents) { - events.add(doc, eventType, delegateListener); - events.add(doc, eventType, delegateUseCapture, true); - } - - if (PointerEvent) { - if (PointerEvent === win.MSPointerEvent) { - pEventTypes = { - up: 'MSPointerUp', down: 'MSPointerDown', over: 'mouseover', - out: 'mouseout', move: 'MSPointerMove', cancel: 'MSPointerCancel' }; - } - else { - pEventTypes = { - up: 'pointerup', down: 'pointerdown', over: 'pointerover', - out: 'pointerout', move: 'pointermove', cancel: 'pointercancel' }; - } - - events.add(doc, pEventTypes.down , listeners.selectorDown ); - events.add(doc, pEventTypes.move , listeners.pointerMove ); - events.add(doc, pEventTypes.over , listeners.pointerOver ); - events.add(doc, pEventTypes.out , listeners.pointerOut ); - events.add(doc, pEventTypes.up , listeners.pointerUp ); - events.add(doc, pEventTypes.cancel, listeners.pointerCancel); - - // autoscroll - events.add(doc, pEventTypes.move, listeners.autoScrollMove); - } - else { - events.add(doc, 'mousedown', listeners.selectorDown); - events.add(doc, 'mousemove', listeners.pointerMove ); - events.add(doc, 'mouseup' , listeners.pointerUp ); - events.add(doc, 'mouseover', listeners.pointerOver ); - events.add(doc, 'mouseout' , listeners.pointerOut ); - - events.add(doc, 'touchstart' , listeners.selectorDown ); - events.add(doc, 'touchmove' , listeners.pointerMove ); - events.add(doc, 'touchend' , listeners.pointerUp ); - events.add(doc, 'touchcancel', listeners.pointerCancel); - - // autoscroll - events.add(doc, 'mousemove', listeners.autoScrollMove); - events.add(doc, 'touchmove', listeners.autoScrollMove); - } - - events.add(win, 'blur', endAllInteractions); - - try { - if (win.frameElement) { - var parentDoc = win.frameElement.ownerDocument, - parentWindow = parentDoc.defaultView; - - events.add(parentDoc , 'mouseup' , listeners.pointerEnd); - events.add(parentDoc , 'touchend' , listeners.pointerEnd); - events.add(parentDoc , 'touchcancel' , listeners.pointerEnd); - events.add(parentDoc , 'pointerup' , listeners.pointerEnd); - events.add(parentDoc , 'MSPointerUp' , listeners.pointerEnd); - events.add(parentWindow, 'blur' , endAllInteractions ); - } - } - catch (error) { - interact.windowParentError = error; - } - - // prevent native HTML5 drag on interact.js target elements - events.add(doc, 'dragstart', function (event) { - for (var i = 0; i < interactions.length; i++) { - var interaction = interactions[i]; - - if (interaction.element - && (interaction.element === event.target - || nodeContains(interaction.element, event.target))) { - - interaction.checkAndPreventDefault(event, interaction.target, interaction.element); - return; - } - } - }); - - if (events.useAttachEvent) { - // For IE's lack of Event#preventDefault - events.add(doc, 'selectstart', function (event) { - var interaction = interactions[0]; - - if (interaction.currentAction()) { - interaction.checkAndPreventDefault(event); - } - }); - - // For IE's bad dblclick event sequence - events.add(doc, 'dblclick', doOnInteractions('ie8Dblclick')); - } - - documents.push(doc); - } - - listenToDocument(document); - - function indexOf (array, target) { - for (var i = 0, len = array.length; i < len; i++) { - if (array[i] === target) { - return i; - } - } - - return -1; - } - - function contains (array, target) { - return indexOf(array, target) !== -1; - } - - function matchesSelector (element, selector, nodeList) { - if (ie8MatchesSelector) { - return ie8MatchesSelector(element, selector, nodeList); - } - - // remove /deep/ from selectors if shadowDOM polyfill is used - if (window !== realWindow) { - selector = selector.replace(/\/deep\//g, ' '); - } - - return element[prefixedMatchesSelector](selector); - } - - function matchesUpTo (element, selector, limit) { - while (isElement(element)) { - if (matchesSelector(element, selector)) { - return true; - } - - element = parentElement(element); - - if (element === limit) { - return matchesSelector(element, selector); - } - } - - return false; - } - - // For IE8's lack of an Element#matchesSelector - // taken from http://tanalin.com/en/blog/2012/12/matches-selector-ie8/ and modified - if (!(prefixedMatchesSelector in Element.prototype) || !isFunction(Element.prototype[prefixedMatchesSelector])) { - ie8MatchesSelector = function (element, selector, elems) { - elems = elems || element.parentNode.querySelectorAll(selector); - - for (var i = 0, len = elems.length; i < len; i++) { - if (elems[i] === element) { - return true; - } - } - - return false; - }; - } - - // requestAnimationFrame polyfill - (function() { - var lastTime = 0, - vendors = ['ms', 'moz', 'webkit', 'o']; - - for(var x = 0; x < vendors.length && !realWindow.requestAnimationFrame; ++x) { - reqFrame = realWindow[vendors[x]+'RequestAnimationFrame']; - cancelFrame = realWindow[vendors[x]+'CancelAnimationFrame'] || realWindow[vendors[x]+'CancelRequestAnimationFrame']; - } - - if (!reqFrame) { - reqFrame = function(callback) { - var currTime = new Date().getTime(), - timeToCall = Math.max(0, 16 - (currTime - lastTime)), - id = setTimeout(function() { callback(currTime + timeToCall); }, - timeToCall); - lastTime = currTime + timeToCall; - return id; - }; - } - - if (!cancelFrame) { - cancelFrame = function(id) { - clearTimeout(id); - }; - } - }()); - - /* global exports: true, module, define */ - - // http://documentcloud.github.io/underscore/docs/underscore.html#section-11 - if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - exports = module.exports = interact; - } - exports.interact = interact; - } - // AMD - else if (typeof define === 'function' && define.amd) { - define('interact', function() { - return interact; - }); - } - else { - realWindow.interact = interact; - } - -} (typeof window === 'undefined'? undefined : window)); diff --git a/interact.min.js b/interact.min.js deleted file mode 100644 index a3dcff69d..000000000 --- a/interact.min.js +++ /dev/null @@ -1,124 +0,0 @@ -/* interact.js v1.2.6 | https://raw.github.com/taye/interact.js/master/LICENSE */ (function(F){function ma(){}function t(a){if(!a||"object"!==typeof a)return!1;var b=V(a)||q;return/object|function/.test(typeof b.Element)?a instanceof b.Element:1===a.nodeType&&"string"===typeof a.nodeName}function Ba(a){return a===q||!(!a||!a.Window)&&a instanceof a.Window}function da(a){return z(a)&&void 0!==typeof a.length&&A(a.splice)}function z(a){return!!a&&"object"===typeof a}function A(a){return"function"===typeof a}function K(a){return"number"===typeof a}function H(a){return"boolean"=== -typeof a}function N(a){return"string"===typeof a}function ea(a){if(!N(a))return!1;Q.querySelector(a);return!0}function x(a,b){for(var c in b)a[c]=b[c];return a}function ra(a,b){for(var c in b){var d=!1,e;for(e in Ca)if(0===c.indexOf(e)&&Ca[e].test(c)){d=!0;break}d||(a[c]=b[c])}return a}function sa(a,b){a.page=a.page||{};a.page.x=b.page.x;a.page.y=b.page.y;a.client=a.client||{};a.client.x=b.client.x;a.client.y=b.client.y;a.timeStamp=b.timeStamp}function Ta(a,b,c){a.page.x=c.page.x-b.page.x;a.page.y= -c.page.y-b.page.y;a.client.x=c.client.x-b.client.x;a.client.y=c.client.y-b.client.y;a.timeStamp=(new Date).getTime()-b.timeStamp;b=Math.max(a.timeStamp/1E3,.001);a.page.speed=fa(a.page.x,a.page.y)/b;a.page.vx=a.page.x/b;a.page.vy=a.page.y/b;a.client.speed=fa(a.client.x,a.page.y)/b;a.client.vx=a.client.x/b;a.client.vy=a.client.y/b}function Ua(a){return a instanceof q.Event||ga&&q.Touch&&a instanceof q.Touch}function ta(a,b,c){c=c||{};a=a||"page";c.x=b[a+"X"];c.y=b[a+"Y"];return c}function Da(a,b){b= -b||{};Va&&Ua(a)?(ta("screen",a,b),b.x+=q.scrollX,b.y+=q.scrollY):ta("page",a,b);return b}function Wa(a,b){b=b||{};Va&&Ua(a)?ta("screen",a,b):ta("client",a,b);return b}function O(a){return K(a.pointerId)?a.pointerId:a.identifier}function Ea(a){return a instanceof nb?a.correspondingUseElement:a}function V(a){if(Ba(a))return a;a=a.ownerDocument||a;return a.defaultView||a.parentWindow||q}function Fa(a){return(a=a instanceof Xa?a.getBoundingClientRect():a.getClientRects()[0])&&{left:a.left,right:a.right, -top:a.top,bottom:a.bottom,width:a.width||a.right-a.left,height:a.height||a.bottom-a.top}}function ua(a){var b,c=Fa(a);!ob&&c&&(b=(b=V(a))||q,a=b.scrollX||b.document.documentElement.scrollLeft,b=b.scrollY||b.document.documentElement.scrollTop,c.left+=a,c.right+=a,c.top+=b,c.bottom+=b);return c}function Ga(a){var b=[];da(a)?(b[0]=a[0],b[1]=a[1]):"touchend"===a.type?1===a.touches.length?(b[0]=a.touches[0],b[1]=a.changedTouches[0]):0===a.touches.length&&(b[0]=a.changedTouches[0],b[1]=a.changedTouches[1]): -(b[0]=a.touches[0],b[1]=a.touches[1]);return b}function Ya(a){for(var b={pageX:0,pageY:0,clientX:0,clientY:0,screenX:0,screenY:0},c,d=0;db?d+=360+d/360|0:-135>b&&(d+=180+d/360|0));return d}function na(a,b){var c=a?a.options.origin:D.origin;"parent"===c?c=L(b):"self"===c?c=a.getRect(b):ea(c)&&(c=Ka(b,c)||{x:0,y:0});A(c)&&(c=c(a&&b));t(c)&&(c=ua(c));c.x="x"in c?c.x:c.left;c.y="y"in -c?c.y:c.top;return c}function Za(a,b,c,d){var e=1-a;return e*e*b+2*e*a*c+a*a*d}function Y(a,b){for(;b;){if(b===a)return!0;b=b.parentNode}return!1}function Ka(a,b){for(var c=L(a);t(c);){if(R(c,b))return c;c=L(c)}return null}function L(a){if((a=a.parentNode)&&a instanceof $a)for(;(a=a.host)&&a&&a instanceof $a;);return a}function va(a,b){return a._context===b.ownerDocument||Y(a._context,b)}function Z(a,b,c){return(a=a.options.ignoreFrom)&&t(c)?N(a)?La(c,a,b):t(a)?Y(a,c):!1:!1}function aa(a,b,c){return(a= -a.options.allowFrom)?t(c)?N(a)?La(c,a,b):t(a)?Y(a,c):!1:!1:!0}function ab(a,b){if(!b)return!1;var c=b.options.drag.axis;return"xy"===a||"xy"===c||c===a}function Ma(a,b){var c=a.options;/^resize/.test(b)&&(b="resize");return c[b].snap&&c[b].snap.enabled}function Na(a,b){var c=a.options;/^resize/.test(b)&&(b="resize");return c[b].restrict&&c[b].restrict.enabled}function ha(a,b,c){for(var d=a.options,e=d[c.name].max,d=d[c.name].maxPerElement,h=0,f=0,g=0,k=0,y=r.length;k=wa||n.target===a&&(f+=u===c.name|0,f>=e||n.element===b&&(g++,u!==c.name||g>=d))))return!1}return 0(new Date).getTime()-db)return}if(c=bb(b,b.type,d))c._updateEventTargets(d,e),c[a](b,b,d,e)}}}function G(a,b,c,d,e,h){var f,g,k=a.target,y=a.snapStatus,n=a.restrictStatus,u=a.pointers,E=(k&&k.options||D).deltaSource,eb=E+"X",l=E+"Y",ia=k?k.options:D,w=na(k,e),m="start"===d,p="end"===d;f=m?a.startCoords:a.curCoords;e=e||a.element;g=x({},f.page);f=x({},f.client);g.x-=w.x;g.y-=w.y;f.x-=w.x;f.y-=w.y;var I=ia[c].snap&&ia[c].snap.relativePoints;!Ma(k,c)||m&&I&&I.length||(this.snap={range:y.range, -locked:y.locked,x:y.snappedX,y:y.snappedY,realX:y.realX,realY:y.realY,dx:y.dx,dy:y.dy},y.locked&&(g.x+=y.dx,g.y+=y.dy,f.x+=y.dx,f.y+=y.dy));!Na(k,c)||m&&ia[c].restrict.elementRect||!n.restricted||(g.x+=n.dx,g.y+=n.dy,f.x+=n.dx,f.y+=n.dy,this.restrict={dx:n.dx,dy:n.dy});this.pageX=g.x;this.pageY=g.y;this.clientX=f.x;this.clientY=f.y;this.x0=a.startCoords.page.x-w.x;this.y0=a.startCoords.page.y-w.y;this.clientX0=a.startCoords.client.x-w.x;this.clientY0=a.startCoords.client.y-w.y;this.ctrlKey=b.ctrlKey; -this.altKey=b.altKey;this.shiftKey=b.shiftKey;this.metaKey=b.metaKey;this.button=b.button;this.buttons=b.buttons;this.target=e;this.t0=a.downTimes[0];this.type=c+(d||"");this.interaction=a;this.interactable=k;e=a.inertiaStatus;e.active&&(this.detail="inertia");h&&(this.relatedTarget=h);p?"client"===E?(this.dx=f.x-a.startCoords.client.x,this.dy=f.y-a.startCoords.client.y):(this.dx=g.x-a.startCoords.page.x,this.dy=g.y-a.startCoords.page.y):m?this.dy=this.dx=0:"inertiastart"===d?(this.dx=a.prevEvent.dx, -this.dy=a.prevEvent.dy):"client"===E?(this.dx=f.x-a.prevEvent.clientX,this.dy=f.y-a.prevEvent.clientY):(this.dx=g.x-a.prevEvent.pageX,this.dy=g.y-a.prevEvent.pageY);a.prevEvent&&"inertia"===a.prevEvent.detail&&!e.active&&ia[c].inertia&&ia[c].inertia.zeroResumeDelta&&(e.resumeDx+=this.dx,e.resumeDy+=this.dy,this.dx=this.dy=0);"resize"===c&&a.resizeAxes?ia.resize.square?("y"===a.resizeAxes?this.dx=this.dy:this.dy=this.dx,this.axes="xy"):(this.axes=a.resizeAxes,"x"===a.resizeAxes?this.dy=0:"y"===a.resizeAxes&& -(this.dx=0)):"gesture"===c&&(this.touches=[u[0],u[1]],m?(this.distance=Ia(u,E),this.box=Ha(u),this.scale=1,this.ds=0,this.angle=Ja(u,void 0,E),this.da=0):p||b instanceof G?(this.distance=a.prevEvent.distance,this.box=a.prevEvent.box,this.scale=a.prevEvent.scale,this.ds=this.scale-1,this.angle=a.prevEvent.angle,this.da=this.angle-a.gesture.startAngle):(this.distance=Ia(u,E),this.box=Ha(u),this.scale=this.distance/a.gesture.startDistance,this.angle=Ja(u,a.gesture.prevAngle,E),this.ds=this.scale-a.gesture.prevScale, -this.da=this.angle-a.gesture.prevAngle));m?(this.timeStamp=a.downTimes[0],this.velocityY=this.velocityX=this.speed=this.duration=this.dt=0):"inertiastart"===d?(this.timeStamp=a.prevEvent.timeStamp,this.dt=a.prevEvent.dt,this.duration=a.prevEvent.duration,this.speed=a.prevEvent.speed,this.velocityX=a.prevEvent.velocityX,this.velocityY=a.prevEvent.velocityY):(this.timeStamp=(new Date).getTime(),this.dt=this.timeStamp-a.prevEvent.timeStamp,this.duration=this.timeStamp-a.downTimes[0],b instanceof G?(b= -this[eb]-a.prevEvent[eb],l=this[l]-a.prevEvent[l],c=this.dt/1E3,this.speed=fa(b,l)/c,this.velocityX=b/c,this.velocityY=l/c):(this.speed=a.pointerDelta[E].speed,this.velocityX=a.pointerDelta[E].vx,this.velocityY=a.pointerDelta[E].vy));(p||"inertiastart"===d)&&600this.timeStamp-a.prevEvent.timeStamp&&(d=180*Math.atan2(a.prevEvent.velocityY,a.prevEvent.velocityX)/Math.PI,0>d&&(d+=360),p=112.5<=d&&247.5>d,l=202.5<=d&&337.5>d,this.swipe={up:l,down:!l&&22.5<=d&&157.5>d,left:p,right:!p&& -(292.5<=d||67.5>d),angle:d,speed:a.prevEvent.speed,velocity:{x:a.prevEvent.velocityX,y:a.prevEvent.velocityY}})}function fb(){this.originalEvent.preventDefault()}function gb(a){var b="";"drag"===a.name&&(b=za.drag);if("resize"===a.name)if(a.axis)b=za[a.name+a.axis];else if(a.edges){for(var b="resize",c=["top","bottom","left","right"],d=0;4>d;d++)a.edges[c[d]]&&(b+=c[d]);b=za[b]}return b}function hb(a,b,c){a=this.getRect(c);var d=!1,e=null,h=null,f,g=x({},b.curCoords.page),e=this.options;if(!a)return null; -if(S.resize&&e.resize.enabled)if(d=e.resize,f={left:!1,right:!1,top:!1,bottom:!1},z(d.edges)){for(var k in f){var y=f,n=k,u;a:{u=k;var E=d.edges[k],l=g,m=b._eventTarget,p=c,w=a,ya=d.margin||pa;if(E){if(!0===E){var q=K(w.width)?w.width:w.right-w.left,I=K(w.height)?w.height:w.bottom-w.top;0>q&&("left"===u?u="right":"right"===u&&(u="left"));0>I&&("top"===u?u="bottom":"bottom"===u&&(u="top"));if("left"===u){u=l.x<(0<=q?w.left:w.right)+ya;break a}if("top"===u){u=l.y<(0<=I?w.top:w.bottom)+ya;break a}if("right"=== -u){u=l.x>(0<=q?w.right:w.left)-ya;break a}if("bottom"===u){u=l.y>(0<=I?w.bottom:w.top)-ya;break a}}u=t(m)?t(E)?E===m:La(m,E,p):!1}else u=!1}y[n]=u}f.left=f.left&&!f.right;f.top=f.top&&!f.bottom;d=f.left||f.right||f.top||f.bottom}else c="y"!==e.resize.axis&&g.x>a.right-pa,a="x"!==e.resize.axis&&g.y>a.bottom-pa,d=c||a,h=(c?"x":"")+(a?"y":"");e=d?"resize":S.drag&&e.drag.enabled?"drag":null;S.gesture&&2<=b.pointerIds.length&&!b.dragging&&!b.resizing&&(e="gesture");return e?{name:e,axis:h,edges:f}:null} -function W(a,b){if(!z(a))return null;var c=a.name,d=b.options;return("resize"===c&&d.resize.enabled||"drag"===c&&d.drag.enabled||"gesture"===c&&d.gesture.enabled)&&S[c]?a:null}function qa(a,b){var c={},d=P[a.type],e=Ea(a.path?a.path[0]:a.target),h=e;b=b?!0:!1;for(var f in a)c[f]=a[f];c.originalEvent=a;for(c.preventDefault=fb;t(h);){for(f=0;fthis.pointerIds.length&&(f=null),this.prepared.name=f.name,this.prepared.axis=f.axis,this.prepared.edges=f.edges,this.snapStatus.snappedX=this.snapStatus.snappedY=this.restrictStatus.restrictedX=this.restrictStatus.restrictedY=NaN,this.downTimes[h]=(new Date).getTime(),this.downTargets[h]=c,ra(this.downPointer,a),sa(this.prevCoords,this.startCoords),this.pointerWasMoved=!1,this.checkAndPreventDefault(b,g,this.element)))}},setModifications:function(a, -b){var c=this.target,d=!0,e=Ma(c,this.prepared.name)&&(!c.options[this.prepared.name].snap.endOnly||b),c=Na(c,this.prepared.name)&&(!c.options[this.prepared.name].restrict.endOnly||b);e?this.setSnapping(a):this.snapStatus.locked=!1;c?this.setRestriction(a):this.restrictStatus.restricted=!1;e&&this.snapStatus.locked&&!this.snapStatus.changed?d=c&&this.restrictStatus.restricted&&this.restrictStatus.changed:c&&this.restrictStatus.restricted&&!this.restrictStatus.changed&&(d=!1);return d},setStartOffsets:function(a, -b,c){a=b.getRect(c);var d=na(b,c);c=b.options[this.prepared.name].snap;b=b.options[this.prepared.name].restrict;var e,h;a?(this.startOffset.left=this.startCoords.page.x-a.left,this.startOffset.top=this.startCoords.page.y-a.top,this.startOffset.right=a.right-this.startCoords.page.x,this.startOffset.bottom=a.bottom-this.startCoords.page.y,e="width"in a?a.width:a.right-a.left,h="height"in a?a.height:a.bottom-a.top):this.startOffset.left=this.startOffset.top=this.startOffset.right=this.startOffset.bottom= -0;this.snapOffsets.splice(0);d=c&&"startCoords"===c.offset?{x:this.startCoords.page.x-d.x,y:this.startCoords.page.y-d.y}:c&&c.offset||{x:0,y:0};if(a&&c&&c.relativePoints&&c.relativePoints.length)for(var f=0;fQa);d||this.pointerIsDown&&!this.pointerWasMoved||(this.pointerIsDown&&clearTimeout(this.holdTimers[h]),this.collectEventTargets(a,b,c,"move"));if(this.pointerIsDown)if(d&&this.pointerWasMoved&&!e)this.checkAndPreventDefault(b,this.target,this.element);else if(Ta(this.pointerDelta,this.prevCoords,this.curCoords),this.prepared.name){if(this.pointerWasMoved&&(!this.inertiaStatus.active||a instanceof G&&/inertiastart/.test(a.type))){if(!this.interacting()&&(Ta(this.pointerDelta, -this.prevCoords,this.curCoords),"drag"===this.prepared.name)){f=Math.abs(f);g=Math.abs(g);d=this.target.options.drag.axis;var k=f>g?"x":fk.bottom&&(b=k.top,k.top=k.bottom,k.bottom=b),k.left>k.right&&(b=k.left,k.left=k.right,k.right=b))):(k.top=Math.min(g.top,f.bottom),k.bottom=Math.max(g.bottom,f.top),k.left=Math.min(g.left,f.right),k.right=Math.max(g.right,f.left));k.width=k.right-k.left;k.height=k.bottom-k.top;for(var q in k)l[q]=k[q]-n[q];a.edges=this.prepared.edges;a.rect=k;a.deltaRect=l}this.target.fire(a); -return a},gestureStart:function(a){a=new G(this,a,"gesture","start",this.element);a.ds=0;this.gesture.startDistance=this.gesture.prevDistance=a.distance;this.gesture.startAngle=this.gesture.prevAngle=a.angle;this.gesture.scale=1;this.gesturing=!0;this.target.fire(a);return a},gestureMove:function(a){if(!this.pointerIds.length)return this.prevEvent;a=new G(this,a,"gesture","move",this.element);a.ds=a.scale-this.gesture.scale;this.target.fire(a);this.gesture.prevAngle=a.angle;this.gesture.prevDistance= -a.distance;Infinity===a.scale||null===a.scale||void 0===a.scale||isNaN(a.scale)||(this.gesture.scale=a.scale);return a},pointerHold:function(a,b,c){this.collectEventTargets(a,b,c,"hold")},pointerUp:function(a,b,c,d){var e=this.mouse?0:v(this.pointerIds,O(a));clearTimeout(this.holdTimers[e]);this.collectEventTargets(a,b,c,"up");this.collectEventTargets(a,b,c,"tap");this.pointerEnd(a,b,c,d);this.removePointer(a)},pointerCancel:function(a,b,c,d){var e=this.mouse?0:v(this.pointerIds,O(a));clearTimeout(this.holdTimers[e]); -this.collectEventTargets(a,b,c,"cancel");this.pointerEnd(a,b,c,d);this.removePointer(a)},ie8Dblclick:function(a,b,c){this.prevTap&&b.clientX===this.prevTap.clientX&&b.clientY===this.prevTap.clientY&&c===this.prevTap.target&&(this.downTargets[0]=c,this.downTimes[0]=(new Date).getTime(),this.collectEventTargets(a,b,c,"tap"))},pointerEnd:function(a,b,c,d){var e,h=this.target,f=h&&h.options,g=f&&this.prepared.name&&f[this.prepared.name].inertia;e=this.inertiaStatus;if(this.interacting()){if(e.active&& -!e.ending)return;var k=(new Date).getTime(),l=!1,m=!1,n=!1,p=Ma(h,this.prepared.name)&&f[this.prepared.name].snap.endOnly,q=Na(h,this.prepared.name)&&f[this.prepared.name].restrict.endOnly,r=0,t=0,f=this.dragging?"x"===f.drag.axis?Math.abs(this.pointerDelta.client.vx):"y"===f.drag.axis?Math.abs(this.pointerDelta.client.vy):this.pointerDelta.client.speed:this.pointerDelta.client.speed,m=(l=g&&g.enabled&&"gesture"!==this.prepared.name&&b!==e.startEvent)&&50>k-this.curCoords.timeStamp&&f>g.minSpeed&& -f>g.endSpeed;l&&!m&&(p||q)&&(g={},g.snap=g.restrict=g,p&&(this.setSnapping(this.curCoords.page,g),g.locked&&(r+=g.dx,t+=g.dy)),q&&(this.setRestriction(this.curCoords.page,g),g.restricted&&(r+=g.dx,t+=g.dy)),r||t)&&(n=!0);if(m||n){sa(e.upCoords,this.curCoords);this.pointers[0]=e.startEvent=new G(this,b,this.prepared.name,"inertiastart",this.element);e.t0=k;h.fire(e.startEvent);m?(e.vx0=this.pointerDelta.client.vx,e.vy0=this.pointerDelta.client.vy,e.v0=f,this.calcInertia(e),b=x({},this.curCoords.page), -h=na(h,this.element),b.x=b.x+e.xe-h.x,b.y=b.y+e.ye-h.y,h={useStatusXY:!0,x:b.x,y:b.y,dx:0,dy:0,snap:null},h.snap=h,r=t=0,p&&(b=this.setSnapping(this.curCoords.page,h),b.locked&&(r+=b.dx,t+=b.dy)),q&&(h=this.setRestriction(this.curCoords.page,h),h.restricted&&(r+=h.dx,t+=h.dy)),e.modifiedXe+=r,e.modifiedYe+=t,e.i=T(this.boundInertiaFrame)):(e.smoothEnd=!0,e.xe=r,e.ye=t,e.sx=e.sy=0,e.i=T(this.boundSmoothEndFrame));e.active=!0;return}(p||q)&&this.pointerMove(a,b,c,d,!0)}this.dragging?(e=new G(this,b, -"drag","end",this.element),q=this.getDrop(e,b,this.element),this.dropTarget=q.dropzone,this.dropElement=q.element,q=this.getDropEvents(b,e),q.leave&&this.prevDropTarget.fire(q.leave),q.enter&&this.dropTarget.fire(q.enter),q.drop&&this.dropTarget.fire(q.drop),q.deactivate&&this.fireActiveDrops(q.deactivate),h.fire(e)):this.resizing?(e=new G(this,b,"resize","end",this.element),h.fire(e)):this.gesturing&&(e=new G(this,b,"gesture","end",this.element),h.fire(e));this.stop(b)},collectDrops:function(a){var b= -[],c=[],d;a=a||this.element;for(d=0;dk),g["double"]=l,this.tapTime=g.timeStamp);for(a=0;ah.innerWidth-p.margin, -a=a.clientY>h.innerHeight-p.margin):(h=Fa(h),d=a.clientXh.right-p.margin,a=a.clientY>h.bottom-p.margin);p.x=c?1:d?-1:0;p.y=a?1:b?-1:0;p.isScrolling||(p.margin=e.margin,p.speed=e.speed,p.start(this))}},_updateEventTargets:function(a,b){this._eventTarget=a;this._curEventTarget=b}};G.prototype={preventDefault:ma,stopImmediatePropagation:function(){this.immediatePropagationStopped=this.propagationStopped=!0},stopPropagation:function(){this.propagationStopped= -!0}};for(var m={},lb="dragStart dragMove resizeStart resizeMove gestureStart gestureMove pointerOver pointerOut pointerHover selectorDown pointerDown pointerMove pointerUp pointerCancel pointerEnd addPointer removePointer recordPointer autoScrollMove".split(" "),Ra=0,Sa=lb.length;Rah.left&&k.xh.top&&k.y=h.left&&f<=h.right&&l>=h.top&&l<=h.bottom;K(g)&&(f=Math.max(0,Math.min(h.right,k.right)-Math.max(h.left,k.left))*Math.max(0,Math.min(h.bottom,k.bottom)-Math.max(h.top,k.top))/(k.width*k.height)>=g);this.options.drop.checker&&(f=this.options.drop.checker(a,b,f,this,e,c,d));return f},dropChecker:function(a){return A(a)?(this.options.drop.checker=a,this): -null===a?(delete this.options.getRect,this):this.options.drop.checker},accept:function(a){return t(a)||ea(a)?(this.options.drop.accept=a,this):null===a?(delete this.options.drop.accept,this):this.options.drop.accept},resizable:function(a){return z(a)?(this.options.resize.enabled=!1===a.enabled?!1:!0,this.setPerAction("resize",a),this.setOnEvents("resize",a),/^x$|^y$|^xy$/.test(a.axis)?this.options.resize.axis=a.axis:null===a.axis&&(this.options.resize.axis=D.resize.axis),H(a.preserveAspectRatio)? -this.options.resize.preserveAspectRatio=a.preserveAspectRatio:H(a.square)&&(this.options.resize.square=a.square),this):H(a)?(this.options.resize.enabled=a,this):this.options.resize},squareResize:function(a){return H(a)?(this.options.resize.square=a,this):null===a?(delete this.options.resize.square,this):this.options.resize.square},gesturable:function(a){return z(a)?(this.options.gesture.enabled=!1===a.enabled?!1:!0,this.setPerAction("gesture",a),this.setOnEvents("gesture",a),this):H(a)?(this.options.gesture.enabled= -a,this):this.options.gesture},autoScroll:function(a){z(a)?a=x({actions:["drag","resize"]},a):H(a)&&(a={actions:["drag","resize"],enabled:a});return this.setOptions("autoScroll",a)},snap:function(a){a=this.setOptions("snap",a);return a===this?this:a.drag},setOptions:function(a,b){var c=b&&da(b.actions)?b.actions:["drag"],d;if(z(b)||H(b)){for(d=0;d 600 + && this.timeStamp - interaction.prevEvent.timeStamp < 150) { + + 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; + + this.swipe = { + up, + down, + left, + right, + angle, + speed: interaction.prevEvent.speed, + velocity: { + x: interaction.prevEvent.velocityX, + y: interaction.prevEvent.velocityY, + }, + }; + } + } + + preventDefault () {} + + stopImmediatePropagation () { + this.immediatePropagationStopped = this.propagationStopped = true; + } + + stopPropagation () { + this.propagationStopped = true; + } +} + +signals.on('interactevent-new', function ({ iEvent, interaction, action, phase, ending, starting, + page, client, deltaSource }) { + // end event dx, dy is difference between start and end points + if (ending) { + if (deltaSource === 'client') { + iEvent.dx = client.x - interaction.startCoords.client.x; + iEvent.dy = client.y - interaction.startCoords.client.y; + } + else { + iEvent.dx = page.x - interaction.startCoords.page.x; + iEvent.dy = page.y - interaction.startCoords.page.y; + } + } + else if (starting) { + iEvent.dx = 0; + iEvent.dy = 0; + } + // copy properties from previousmove if starting inertia + else if (phase === 'inertiastart') { + iEvent.dx = interaction.prevEvent.dx; + iEvent.dy = interaction.prevEvent.dy; + } + else { + if (deltaSource === 'client') { + iEvent.dx = client.x - interaction.prevEvent.clientX; + iEvent.dy = client.y - interaction.prevEvent.clientY; + } + else { + iEvent.dx = page.x - interaction.prevEvent.pageX; + iEvent.dy = page.y - interaction.prevEvent.pageY; + } + } + + const options = interaction.target.options; + const inertiaStatus = interaction.inertiaStatus; + + if (interaction.prevEvent && interaction.prevEvent.detail === 'inertia' + && !inertiaStatus.active + && options[action].inertia && options[action].inertia.zeroResumeDelta) { + + inertiaStatus.resumeDx += iEvent.dx; + inertiaStatus.resumeDy += iEvent.dy; + + iEvent.dx = iEvent.dy = 0; + } +}); + +module.exports = InteractEvent; diff --git a/src/Interactable.js b/src/Interactable.js new file mode 100644 index 000000000..2ab9367cf --- /dev/null +++ b/src/Interactable.js @@ -0,0 +1,659 @@ +const isType = require('./utils/isType'); +const events = require('./utils/events'); +const signals = require('./utils/signals'); +const extend = require('./utils/extend'); +const actions = require('./actions/base'); +const scope = require('./scope'); + +const { getElementRect } = require('./utils/domUtils'); +const { indexOf, contains } = require('./utils/arr'); + +// all set interactables +scope.interactables = []; + +/*\ + * Interactable + [ property ] + ** + * Object type returned by @interact +\*/ +class Interactable { + constructor (element, options) { + this._element = element; + this._context = scope.document; + this._iEvents = this._iEvents || {}; + + let _window; + + if (isType.trySelector(element)) { + this.selector = element; + + const context = options && options.context; + + _window = context? scope.getWindow(context) : scope.window; + + if (context && (_window.Node + ? context instanceof _window.Node + : (isType.isElement(context) || context === _window.document))) { + + this._context = context; + } + } + else { + _window = scope.getWindow(element); + } + + this._doc = _window.document; + + signals.fire('interactable-new', { + element, + options, + interactable: this, + win: _window, + }); + + if (this._doc !== scope.document) { + signals.fire('listen-to-document', { + doc: this._doc, + win: _window, + }); + } + + scope.interactables.push(this); + + this.set(options); + } + + setOnEvents (action, phases) { + const onAction = 'on' + action; + + if (isType.isFunction(phases.onstart) ) { this[onAction + 'start' ] = phases.onstart ; } + if (isType.isFunction(phases.onmove) ) { this[onAction + 'move' ] = phases.onmove ; } + if (isType.isFunction(phases.onend) ) { this[onAction + 'end' ] = phases.onend ; } + if (isType.isFunction(phases.oninertiastart)) { this[onAction + 'inertiastart' ] = phases.oninertiastart ; } + + return this; + } + + setPerAction (action, options) { + // for all the default per-action options + for (const option in options) { + // if this option exists for this action + if (option in scope.defaultOptions[action]) { + // if the option in the options arg is an object value + if (isType.isObject(options[option])) { + // duplicate the object + this.options[action][option] = extend(this.options[action][option] || {}, options[option]); + + if (isType.isObject(scope.defaultOptions.perAction[option]) && 'enabled' in scope.defaultOptions.perAction[option]) { + this.options[action][option].enabled = options[option].enabled === false? false : true; + } + } + else if (isType.isBool(options[option]) && isType.isObject(scope.defaultOptions.perAction[option])) { + this.options[action][option].enabled = options[option]; + } + else if (options[option] !== undefined) { + // or if it's not undefined, do a plain assignment + this.options[action][option] = options[option]; + } + } + } + } + + getAction (pointer, event, interaction, element) { + const action = this.defaultActionChecker(pointer, event, interaction, element); + + if (this.options.actionChecker) { + return this.options.actionChecker(pointer, event, action, this, element, interaction); + } + + return action; + } + + /*\ + * Interactable.actionChecker + [ method ] + * + * Gets or sets the function used to check action to be performed on + * pointerDown + * + - checker (function | null) #optional 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. + = (Function | Interactable) The checker function or this Interactable + * + | 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; + | }); + \*/ + actionChecker (checker) { + if (isType.isFunction(checker)) { + this.options.actionChecker = checker; + + return this; + } + + if (checker === null) { + delete this.options.actionChecker; + + return this; + } + + return this.options.actionChecker; + } + + /*\ + * Interactable.getRect + [ method ] + * + * The default function to get an Interactables bounding rect. Can be + * overridden using @Interactable.rectChecker. + * + - element (Element) #optional The element to measure. + = (object) The object's bounding rectangle. + o { + o top : 0, + o left : 0, + o bottom: 0, + o right : 0, + o width : 0, + o height: 0 + o } + \*/ + getRect (element) { + element = element || this._element; + + if (this.selector && !(isType.isElement(element))) { + element = this._context.querySelector(this.selector); + } + + return getElementRect(element); + } + + /*\ + * Interactable.rectChecker + [ method ] + * + * Returns or sets the function used to calculate the interactable's + * element's rectangle + * + - checker (function) #optional A function which returns this Interactable's bounding rectangle. See @Interactable.getRect + = (function | object) The checker function or this Interactable + \*/ + rectChecker (checker) { + if (isType.isFunction(checker)) { + this.getRect = checker; + + return this; + } + + if (checker === null) { + delete this.options.getRect; + + return this; + } + + return this.getRect; + } + + /*\ + * Interactable.styleCursor + [ method ] + * + * Returns or sets whether the action that would be performed when the + * mouse on the element are checked on `mousemove` so that the cursor + * may be styled appropriately + * + - newValue (boolean) #optional + = (boolean | Interactable) The current setting or this Interactable + \*/ + styleCursor (newValue) { + if (isType.isBool(newValue)) { + this.options.styleCursor = newValue; + + return this; + } + + if (newValue === null) { + delete this.options.styleCursor; + + return this; + } + + return this.options.styleCursor; + } + + /*\ + * Interactable.preventDefault + [ method ] + * + * 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 + * + - newValue (string) #optional `true`, `false` or `'auto'` + = (string | Interactable) The current setting or this Interactable + \*/ + preventDefault (newValue) { + if (/^(always|never|auto)$/.test(newValue)) { + this.options.preventDefault = newValue; + return this; + } + + if (isType.isBool(newValue)) { + this.options.preventDefault = newValue? 'always' : 'never'; + return this; + } + + return this.options.preventDefault; + } + + /*\ + * Interactable.origin + [ method ] + * + * Gets or sets the origin of the Interactable's element. The x and y + * of the origin will be subtracted from action event coordinates. + * + - origin (object | string) #optional An object eg. { x: 0, y: 0 } or string 'parent', 'self' or any CSS selector + * OR + - origin (Element) #optional An HTML or SVG Element whose rect will be used + ** + = (object) The current origin or this Interactable + \*/ + origin (newValue) { + if (isType.trySelector(newValue)) { + this.options.origin = newValue; + return this; + } + else if (isType.isObject(newValue)) { + this.options.origin = newValue; + return this; + } + + return this.options.origin; + } + + /*\ + * Interactable.deltaSource + [ method ] + * + * Returns or sets the mouse coordinate types used to calculate the + * movement of the pointer. + * + - newValue (string) #optional Use 'client' if you will be scrolling while interacting; Use 'page' if you want autoScroll to work + = (string | object) The current deltaSource or this Interactable + \*/ + deltaSource (newValue) { + if (newValue === 'page' || newValue === 'client') { + this.options.deltaSource = newValue; + + return this; + } + + return this.options.deltaSource; + } + + /*\ + * Interactable.context + [ method ] + * + * Gets the selector context Node of the Interactable. The default is `window.document`. + * + = (Node) The context Node of this Interactable + ** + \*/ + context () { + return this._context; + } + + /*\ + * Interactable.ignoreFrom + [ method ] + * + * 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. + * + - newValue (string | Element | null) #optional a CSS selector string, an Element or `null` to not ignore any elements + = (string | Element | object) The current ignoreFrom value or this Interactable + ** + | interact(element, { ignoreFrom: document.getElementById('no-action') }); + | // or + | interact(element).ignoreFrom('input, textarea, a'); + \*/ + ignoreFrom (newValue) { + if (isType.trySelector(newValue)) { // CSS selector to match event.target + this.options.ignoreFrom = newValue; + return this; + } + + if (isType.isElement(newValue)) { // specific element + this.options.ignoreFrom = newValue; + return this; + } + + return this.options.ignoreFrom; + } + + /*\ + * Interactable.allowFrom + [ method ] + * + * 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. + * + - newValue (string | Element | null) #optional a CSS selector string, an Element or `null` to allow from any element + = (string | Element | object) The current allowFrom value or this Interactable + ** + | interact(element, { allowFrom: document.getElementById('drag-handle') }); + | // or + | interact(element).allowFrom('.handle'); + \*/ + allowFrom (newValue) { + if (isType.trySelector(newValue)) { // CSS selector to match event.target + this.options.allowFrom = newValue; + return this; + } + + if (isType.isElement(newValue)) { // specific element + this.options.allowFrom = newValue; + return this; + } + + return this.options.allowFrom; + } + + /*\ + * Interactable.element + [ method ] + * + * If this is not a selector Interactable, it returns the element this + * interactable represents + * + = (Element) HTML / SVG Element + \*/ + element () { + return this._element; + } + + /*\ + * Interactable.fire + [ method ] + * + * Calls listeners for the given InteractEvent type bound globally + * and directly to this Interactable + * + - iEvent (InteractEvent) The InteractEvent object to be fired on this Interactable + = (Interactable) this Interactable + \*/ + fire (iEvent) { + if (!(iEvent && iEvent.type) || !contains(scope.eventTypes, iEvent.type)) { + return this; + } + + let listeners; + const onEvent = 'on' + iEvent.type; + + // Interactable#on() listeners + if (iEvent.type in this._iEvents) { + listeners = this._iEvents[iEvent.type]; + + for (let i = 0, len = listeners.length; i < len && !iEvent.immediatePropagationStopped; i++) { + listeners[i](iEvent); + } + } + + // interactable.onevent listener + if (isType.isFunction(this[onEvent])) { + this[onEvent](iEvent); + } + + // interact.on() listeners + if (iEvent.type in scope.globalEvents && (listeners = scope.globalEvents[iEvent.type])) { + + for (let i = 0, len = listeners.length; i < len && !iEvent.immediatePropagationStopped; i++) { + listeners[i](iEvent); + } + } + + return this; + } + + /*\ + * Interactable.on + [ method ] + * + * Binds a listener for an InteractEvent or DOM event. + * + - eventType (string | array | object) The types of events to listen for + - listener (function) The function event (s) + - useCapture (boolean) #optional useCapture flag for addEventListener + = (object) This Interactable + \*/ + on (eventType, listener, useCapture) { + if (isType.isString(eventType) && eventType.search(' ') !== -1) { + eventType = eventType.trim().split(/ +/); + } + + if (isType.isArray(eventType)) { + for (let i = 0; i < eventType.length; i++) { + this.on(eventType[i], listener, useCapture); + } + + return this; + } + + if (isType.isObject(eventType)) { + for (const prop in eventType) { + this.on(prop, eventType[prop], listener); + } + + return this; + } + + if (eventType === 'wheel') { + eventType = scope.wheelEvent; + } + + // convert to boolean + useCapture = useCapture? true: false; + + if (contains(scope.eventTypes, eventType)) { + // if this type of event was never bound to this Interactable + if (!(eventType in this._iEvents)) { + this._iEvents[eventType] = [listener]; + } + else { + this._iEvents[eventType].push(listener); + } + } + // delegated event for selector + else if (this.selector) { + events.addDelegate(this.selector, this._context, eventType, listener, useCapture); + } + else { + events.add(this._element, eventType, listener, useCapture); + } + + return this; + } + + /*\ + * Interactable.off + [ method ] + * + * Removes an InteractEvent or DOM event listener + * + - eventType (string | array | object) The types of events that were listened for + - listener (function) The listener function to be removed + - useCapture (boolean) #optional useCapture flag for removeEventListener + = (object) This Interactable + \*/ + off (eventType, listener, useCapture) { + if (isType.isString(eventType) && eventType.search(' ') !== -1) { + eventType = eventType.trim().split(/ +/); + } + + if (isType.isArray(eventType)) { + for (let i = 0; i < eventType.length; i++) { + this.off(eventType[i], listener, useCapture); + } + + return this; + } + + if (isType.isObject(eventType)) { + for (const prop in eventType) { + this.off(prop, eventType[prop], listener); + } + + return this; + } + + + // convert to boolean + useCapture = useCapture? true: false; + + if (eventType === 'wheel') { + eventType = scope.wheelEvent; + } + + // if it is an action event type + if (contains(scope.eventTypes, eventType)) { + const eventList = this._iEvents[eventType]; + const index = eventList? indexOf(eventList, listener) : -1; + + if (index !== -1) { + this._iEvents[eventType].splice(index, 1); + } + } + // delegated event + else if (this.selector) { + events.removeDelegate(this.selector, this._context, eventType, listener, useCapture); + } + // remove listener from this Interatable's element + else { + events.remove(this._element, eventType, listener, useCapture); + } + + return this; + } + + /*\ + * Interactable.set + [ method ] + * + * Reset the options of this Interactable + - options (object) The new settings to apply + = (object) This Interactable + \*/ + set (options) { + if (!isType.isObject(options)) { + options = {}; + } + + this.options = extend({}, scope.defaultOptions.base); + + const perActions = extend({}, scope.defaultOptions.perAction); + + for (const actionName in actions.methodDict) { + const methodName = actions.methodDict[actionName]; + + this.options[actionName] = extend({}, scope.defaultOptions[actionName]); + + this.setPerAction(actionName, perActions); + + this[methodName](options[actionName]); + } + + const settings = [ + 'accept', 'actionChecker', 'allowFrom', 'deltaSource', + 'dropChecker', 'ignoreFrom', 'origin', 'preventDefault', + 'rectChecker', 'styleCursor', + ]; + + for (let i = 0, len = settings.length; i < len; i++) { + const setting = settings[i]; + + this.options[setting] = scope.defaultOptions.base[setting]; + + if (setting in options) { + this[setting](options[setting]); + } + } + + return this; + } + + /*\ + * Interactable.unset + [ method ] + * + * Remove this interactable from the list of interactables and remove + * it's action capabilities and event listeners + * + = (object) @interact + \*/ + unset () { + events.remove(this._element, 'all'); + + if (!isType.isString(this.selector)) { + events.remove(this, 'all'); + if (this.options.styleCursor) { + this._element.style.cursor = ''; + } + } + else { + // remove delegated events + for (const type in events.delegatedEvents) { + const delegated = events.delegatedEvents[type]; + + for (let i = 0; i < delegated.selectors.length; i++) { + if (delegated.selectors[i] === this.selector + && delegated.contexts[i] === this._context) { + + delegated.selectors.splice(i, 1); + delegated.contexts .splice(i, 1); + delegated.listeners.splice(i, 1); + + // remove the arrays if they are empty + if (!delegated.selectors.length) { + delegated[type] = null; + } + } + + events.remove(this._context, type, events.delegateListener); + events.remove(this._context, type, events.delegateUseCapture, true); + + break; + } + } + } + + signals.fire('interactable-unset', { interactable: this }); + + scope.interactables.splice(isType.indexOf(scope.interactables, this), 1); + + return scope.interact; + } +} + +Interactable.prototype.defaultActionChecker = actions.defaultChecker; + +module.exports = Interactable; diff --git a/src/Interaction.js b/src/Interaction.js new file mode 100644 index 000000000..47447618d --- /dev/null +++ b/src/Interaction.js @@ -0,0 +1,1172 @@ +const scope = require('./scope'); +const utils = require('./utils'); +const InteractEvent = require('./InteractEvent'); +const events = require('./utils/events'); +const signals = require('./utils/signals'); +const browser = require('./utils/browser'); +const finder = require('./utils/interactionFinder'); +const actions = require('./actions/base'); +const modifiers = require('./modifiers/base'); +const animationFrame = utils.raf; + +const listeners = {}; +const methodNames = [ + 'pointerOver', 'pointerOut', 'pointerHover', 'selectorDown', + 'pointerDown', 'pointerMove', 'pointerUp', 'pointerCancel', 'pointerEnd', + 'addPointer', 'removePointer', 'recordPointer', +]; + +// for ignoring browser's simulated mouse events +let prevTouchTime = 0; + +// all active and idle interactions +scope.interactions = []; + +class Interaction { + constructor () { + this.target = null; // current interactable being interacted with + this.element = null; // the target element of the interactable + + this.matches = []; // all selectors that are matched by target element + this.matchElements = []; // corresponding elements + + this.prepared = { // action that's ready to be fired on next move event + name : null, + axis : null, + edges: null, + }; + + this.inertiaStatus = { + active : false, + smoothEnd: false, + ending : false, + + startEvent: null, + upCoords : {}, + + xe: 0, ye: 0, + sx: 0, sy: 0, + + t0: 0, + vx0: 0, vys: 0, + duration: 0, + + resumeDx: 0, + resumeDy: 0, + + lambda_v0: 0, + one_ve_v0: 0, + i : null, + }; + + this.boundInertiaFrame = () => this.inertiaFrame (); + this.boundSmoothEndFrame = () => this.smoothEndFrame(); + + // keep track of added pointers + this.pointers = []; + this.pointerIds = []; + this.downTargets = []; + this.downTimes = []; + this.holdTimers = []; + + // Previous native pointer move event coordinates + this.prevCoords = { + page : { x: 0, y: 0 }, + client : { x: 0, y: 0 }, + timeStamp: 0, + }; + // current native pointer move event coordinates + this.curCoords = { + page : { x: 0, y: 0 }, + client : { x: 0, y: 0 }, + timeStamp: 0, + }; + + // Starting InteractEvent pointer coordinates + this.startCoords = { + page : { x: 0, y: 0 }, + client : { x: 0, y: 0 }, + timeStamp: 0, + }; + + // Change in coordinates and time of the pointer + this.pointerDelta = { + page : { x: 0, y: 0, vx: 0, vy: 0, speed: 0 }, + client : { x: 0, y: 0, vx: 0, vy: 0, speed: 0 }, + timeStamp: 0, + }; + + this.downEvent = null; // pointerdown/mousedown/touchstart event + this.downPointer = {}; + + this._eventTarget = null; + this._curEventTarget = null; + + this.prevEvent = null; // previous action event + + this.startOffset = { left: 0, right: 0, top: 0, bottom: 0 }; + this.modifierOffsets = {}; + this.modifierStatuses = modifiers.resetStatuses({}); + + this.pointerIsDown = false; + this.pointerWasMoved = false; + this._interacting = false; + + this.mouse = false; + + signals.fire('interaction-new', this); + + scope.interactions.push(this); + } + + setEventXY (targetObj, pointers) { + const pointer = (pointers.length > 1 + ? utils.pointerAverage(pointers) + : pointers[0]); + + const tmpXY = {}; + + utils.getPageXY(pointer, tmpXY, this); + targetObj.page.x = tmpXY.x; + targetObj.page.y = tmpXY.y; + + utils.getClientXY(pointer, tmpXY, this); + targetObj.client.x = tmpXY.x; + targetObj.client.y = tmpXY.y; + + targetObj.timeStamp = new Date().getTime(); + } + + pointerOver (pointer, event, eventTarget) { + if (this.prepared.name || !this.mouse) { return; } + + const curMatches = []; + const curMatchElements = []; + const prevTargetElement = this.element; + + this.addPointer(pointer); + + if (this.target + && (scope.testIgnore(this.target, this.element, eventTarget) + || !scope.testAllow(this.target, this.element, eventTarget))) { + // if the eventTarget should be ignored or shouldn't be allowed + // clear the previous target + this.target = null; + this.element = null; + this.matches = []; + this.matchElements = []; + } + + const elementInteractable = scope.interactables.get(eventTarget); + let elementAction = (elementInteractable + && !scope.testIgnore(elementInteractable, eventTarget, eventTarget) + && scope.testAllow(elementInteractable, eventTarget, eventTarget) + && validateAction( + elementInteractable.getAction(pointer, event, this, eventTarget), + elementInteractable)); + + if (elementAction && !scope.withinInteractionLimit(elementInteractable, eventTarget, elementAction)) { + elementAction = null; + } + + function pushCurMatches (interactable, selector) { + if (interactable + && scope.inContext(interactable, eventTarget) + && !scope.testIgnore(interactable, eventTarget, eventTarget) + && scope.testAllow(interactable, eventTarget, eventTarget) + && utils.matchesSelector(eventTarget, selector)) { + + curMatches.push(interactable); + curMatchElements.push(eventTarget); + } + } + + if (elementAction) { + this.target = elementInteractable; + this.element = eventTarget; + this.matches = []; + this.matchElements = []; + } + else { + scope.interactables.forEachSelector(pushCurMatches); + + if (this.validateSelector(pointer, event, curMatches, curMatchElements)) { + this.matches = curMatches; + this.matchElements = curMatchElements; + + this.pointerHover(pointer, event, this.matches, this.matchElements); + events.add(eventTarget, + scope.PointerEvent? browser.pEventTypes.move : 'mousemove', + listeners.pointerHover); + } + else if (this.target) { + if (utils.nodeContains(prevTargetElement, eventTarget)) { + this.pointerHover(pointer, event, this.matches, this.matchElements); + events.add(this.element, + scope.PointerEvent? browser.pEventTypes.move : 'mousemove', + listeners.pointerHover); + } + else { + this.target = null; + this.element = null; + this.matches = []; + this.matchElements = []; + } + } + } + } + + // Check what action would be performed on pointerMove target if a mouse + // button were pressed and change the cursor accordingly + pointerHover (pointer, event, eventTarget, curEventTarget, matches, matchElements) { + const target = this.target; + + if (!this.prepared.name && this.mouse) { + + let action; + + // update pointer coords for defaultActionChecker to use + this.setEventXY(this.curCoords, [pointer]); + + if (matches) { + action = this.validateSelector(pointer, event, matches, matchElements); + } + else if (target) { + action = validateAction(target.getAction(this.pointers[0], event, this, this.element), this.target); + } + + if (target && target.options.styleCursor) { + if (action) { + target._doc.documentElement.style.cursor = actions[action.name].getCursor(action); + } + else { + target._doc.documentElement.style.cursor = ''; + } + } + } + else if (this.prepared.name) { + this.checkAndPreventDefault(event, target, this.element); + } + } + + pointerOut (pointer, event, eventTarget) { + if (this.prepared.name) { return; } + + // Remove temporary event listeners for selector Interactables + if (!scope.interactables.get(eventTarget)) { + events.remove(eventTarget, + scope.PointerEvent? browser.pEventTypes.move : 'mousemove', + listeners.pointerHover); + } + + if (this.target && this.target.options.styleCursor && !this.interacting()) { + this.target._doc.documentElement.style.cursor = ''; + } + } + + selectorDown (pointer, event, eventTarget, curEventTarget) { + const pointerIndex = this.addPointer(pointer); + let element = eventTarget; + let action; + + this.pointerIsDown = true; + + signals.fire('interaction-down', { + pointer, + event, + eventTarget, + pointerIndex, + interaction: this, + }); + + // Check if the down event hits the current inertia target + if (this.inertiaStatus.active && this.target.selector) { + // climb up the DOM tree from the event target + while (utils.isElement(element)) { + + // if this element is the current inertia target element + if (element === this.element + // and the prospective action is the same as the ongoing one + && validateAction(this.target.getAction(pointer, event, this, this.element), this.target).name === this.prepared.name) { + + // stop inertia so that the next move will be a normal one + animationFrame.cancel(this.inertiaStatus.i); + this.inertiaStatus.active = false; + + return; + } + element = utils.parentElement(element); + } + } + + // do nothing if interacting + if (this.interacting()) { + return; + } + + const pushMatches = (interactable, selector, context) => { + const elements = (browser.useMatchesSelectorPolyfill + ? context.querySelectorAll(selector) + : undefined); + + if (scope.inContext(interactable, element) + && !scope.testIgnore(interactable, element, eventTarget) + && scope.testAllow(interactable, element, eventTarget) + && utils.matchesSelector(element, selector, elements)) { + + this.matches.push(interactable); + this.matchElements.push(element); + } + }; + + // update pointer coords for defaultActionChecker to use + this.setEventXY(this.curCoords, [pointer]); + this.downEvent = event; + + while (utils.isElement(element) && !action) { + this.matches = []; + this.matchElements = []; + + scope.interactables.forEachSelector(pushMatches); + + action = this.validateSelector(pointer, event, this.matches, this.matchElements); + element = utils.parentElement(element); + } + + if (action) { + this.prepared.name = action.name; + this.prepared.axis = action.axis; + this.prepared.edges = action.edges; + + return this.pointerDown(pointer, event, eventTarget, curEventTarget, action); + } + else { + // do these now since pointerDown isn't being called from here + this.downTimes[pointerIndex] = new Date().getTime(); + this.downTargets[pointerIndex] = eventTarget; + utils.pointerExtend(this.downPointer, pointer); + + utils.copyCoords(this.prevCoords, this.curCoords); + this.pointerWasMoved = false; + } + } + + // Determine action to be performed on next pointerMove and add appropriate + // style and event Listeners + pointerDown (pointer, event, eventTarget, curEventTarget, forceAction) { + if (!forceAction && !this.inertiaStatus.active && this.pointerWasMoved && this.prepared.name) { + this.checkAndPreventDefault(event, this.target, this.element); + + return; + } + + this.pointerIsDown = true; + this.downEvent = event; + + const pointerIndex = this.addPointer(pointer); + let action; + + // If it is the second touch of a multi-touch gesture, keep the + // target the same and get a new action if a target was set by the + // first touch + if (this.pointerIds.length > 1 && this.target._element === this.element) { + const newAction = validateAction(forceAction || this.target.getAction(pointer, event, this, this.element), this.target); + + if (scope.withinInteractionLimit(this.target, this.element, newAction)) { + action = newAction; + } + + this.prepared.name = null; + } + // Otherwise, set the target if there is no action prepared + else if (!this.prepared.name) { + const interactable = scope.interactables.get(curEventTarget); + + if (interactable + && !scope.testIgnore(interactable, curEventTarget, eventTarget) + && scope.testAllow(interactable, curEventTarget, eventTarget) + && (action = validateAction(forceAction || interactable.getAction(pointer, event, this, curEventTarget), interactable, eventTarget)) + && scope.withinInteractionLimit(interactable, curEventTarget, action)) { + this.target = interactable; + this.element = curEventTarget; + } + } + + const target = this.target; + const options = target && target.options; + + if (target && (forceAction || !this.prepared.name)) { + action = action || validateAction(forceAction || target.getAction(pointer, event, this, curEventTarget), target, this.element); + + this.setEventXY(this.startCoords, this.pointers); + + if (!action) { return; } + + if (options.styleCursor) { + target._doc.documentElement.style.cursor = actions[action.name].getCursor(action); + } + + this.resizeAxes = action.name === 'resize'? action.axis : null; + + if (action === 'gesture' && this.pointerIds.length < 2) { + action = null; + } + + this.prepared.name = action.name; + this.prepared.axis = action.axis; + this.prepared.edges = action.edges; + + modifiers.resetStatuses(this.modifierStatuses); + + this.downTimes[pointerIndex] = new Date().getTime(); + this.downTargets[pointerIndex] = eventTarget; + utils.pointerExtend(this.downPointer, pointer); + + utils.copyCoords(this.prevCoords, this.startCoords); + this.pointerWasMoved = false; + + this.checkAndPreventDefault(event, target, this.element); + } + // if inertia is active try to resume action + else if (this.inertiaStatus.active + && curEventTarget === this.element + && validateAction(target.getAction(pointer, event, this, this.element), target).name === this.prepared.name) { + + animationFrame.cancel(this.inertiaStatus.i); + this.inertiaStatus.active = false; + + this.checkAndPreventDefault(event, target, this.element); + } + } + + setStartOffsets (action, interactable, element) { + const rect = interactable.getRect(element); + + if (rect) { + this.startOffset.left = this.startCoords.page.x - rect.left; + this.startOffset.top = this.startCoords.page.y - rect.top; + + this.startOffset.right = rect.right - this.startCoords.page.x; + this.startOffset.bottom = rect.bottom - this.startCoords.page.y; + + if (!('width' in rect)) { rect.width = rect.right - rect.left; } + if (!('height' in rect)) { rect.height = rect.bottom - rect.top ; } + } + else { + this.startOffset.left = this.startOffset.top = this.startOffset.right = this.startOffset.bottom = 0; + } + + modifiers.setOffsets(this, interactable, element, rect, this.modifierOffsets); + } + + /*\ + * Interaction.start + [ method ] + * + * 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) + * + - action (object) The action to be performed - drag, resize, etc. + - interactable (Interactable) The Interactable to target + - element (Element) The DOM Element to target + = (object) interact + ** + | 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 (action, interactable, element) { + if (this.interacting() + || !this.pointerIsDown + || this.pointerIds.length < (action.name === 'gesture'? 2 : 1)) { + return; + } + + // if this interaction had been removed after stopping + // add it back + if (utils.indexOf(scope.interactions, this) === -1) { + scope.interactions.push(this); + } + + // set the startCoords if there was no prepared action + if (!this.prepared.name) { + this.setEventXY(this.startCoords, this.pointers); + } + + this.prepared.name = action.name; + this.prepared.axis = action.axis; + this.prepared.edges = action.edges; + this.target = interactable; + this.element = element; + + this.setStartOffsets(action.name, interactable, element, this.modifierOffsets); + + modifiers.setAll(this, this.startCoords.page, this.modifierStatuses); + + this.prevEvent = actions[this.prepared.name].start(this, this.downEvent); + } + + pointerMove (pointer, event, eventTarget, curEventTarget, preEnd) { + if (this.inertiaStatus.active) { + const pageUp = this.inertiaStatus.upCoords.page; + const clientUp = this.inertiaStatus.upCoords.client; + + this.setEventXY(this.curCoords, [ { + pageX : pageUp.x + this.inertiaStatus.sx, + pageY : pageUp.y + this.inertiaStatus.sy, + clientX: clientUp.x + this.inertiaStatus.sx, + clientY: clientUp.y + this.inertiaStatus.sy, + } ]); + } + else { + this.recordPointer(pointer); + this.setEventXY(this.curCoords, this.pointers); + } + + const duplicateMove = (this.curCoords.page.x === this.prevCoords.page.x + && this.curCoords.page.y === this.prevCoords.page.y + && this.curCoords.client.x === this.prevCoords.client.x + && this.curCoords.client.y === this.prevCoords.client.y); + + let dx; + let dy; + + // register movement greater than pointerMoveTolerance + if (this.pointerIsDown && !this.pointerWasMoved) { + dx = this.curCoords.client.x - this.startCoords.client.x; + dy = this.curCoords.client.y - this.startCoords.client.y; + + this.pointerWasMoved = utils.hypot(dx, dy) > scope.pointerMoveTolerance; + } + + signals.fire('interaction-move', { + pointer, + event, + eventTarget, + dx, + dy, + interaction: this, + duplicate: duplicateMove, + }); + + if (!this.pointerIsDown) { return; } + + if (duplicateMove && this.pointerWasMoved && !preEnd) { + this.checkAndPreventDefault(event, this.target, this.element); + return; + } + + // set pointer coordinate, time changes and speeds + utils.setEventDeltas(this.pointerDelta, this.prevCoords, this.curCoords); + + if (!this.prepared.name) { return; } + + if (this.pointerWasMoved + // ignore movement while inertia is active + && (!this.inertiaStatus.active || (pointer instanceof InteractEvent && /inertiastart/.test(pointer.type)))) { + + // if just starting an action, calculate the pointer speed now + if (!this.interacting()) { + utils.setEventDeltas(this.pointerDelta, this.prevCoords, this.curCoords); + + actions[this.prepared.name].beforeStart(this, pointer, event, eventTarget, curEventTarget, dx, dy); + } + + const starting = !!this.prepared.name && !this.interacting(); + + if (starting + && (this.target.options[this.prepared.name].manualStart + || !scope.withinInteractionLimit(this.target, this.element, this.prepared))) { + this.stop(event); + return; + } + + if (this.prepared.name && this.target) { + if (starting) { + this.start(this.prepared, this.target, this.element); + } + + const modifierResult = modifiers.setAll(this, this.curCoords.page, this.modifierStatuses, preEnd); + + // move if snapping or restriction doesn't prevent it + if (modifierResult.shouldMove || starting) { + this.prevEvent = actions[this.prepared.name].move(this, event); + } + + this.checkAndPreventDefault(event, this.target, this.element); + } + } + + utils.copyCoords(this.prevCoords, this.curCoords); + + signals.fire('interaction-move-done', { + pointer, + event, + interaction: this, + }); + } + + pointerUp (pointer, event, eventTarget, curEventTarget) { + const pointerIndex = this.mouse? 0 : utils.indexOf(this.pointerIds, utils.getPointerId(pointer)); + + clearTimeout(this.holdTimers[pointerIndex]); + + signals.fire('interaction-up', { + pointer, + event, + eventTarget, + curEventTarget, + interaction: this, + }); + + + this.pointerEnd(pointer, event, eventTarget, curEventTarget); + + this.removePointer(pointer); + } + + pointerCancel (pointer, event, eventTarget, curEventTarget) { + const pointerIndex = this.mouse? 0 : utils.indexOf(this.pointerIds, utils.getPointerId(pointer)); + + clearTimeout(this.holdTimers[pointerIndex]); + + signals.fire('interaction-cancel', { + pointer, + event, + eventTarget, + interaction: this, + }); + + this.pointerEnd(pointer, event, eventTarget, curEventTarget); + + this.removePointer(pointer); + } + + // End interact move events and stop auto-scroll unless inertia is enabled + pointerEnd (pointer, event, eventTarget, curEventTarget) { + const target = this.target; + const options = target && target.options; + const inertiaOptions = options && this.prepared.name && options[this.prepared.name].inertia; + const inertiaStatus = this.inertiaStatus; + + if (this.interacting()) { + + if (inertiaStatus.active && !inertiaStatus.ending) { return; } + + const now = new Date().getTime(); + const statuses = {}; + const page = utils.extend({}, this.curCoords.page); + let pointerSpeed; + let inertiaPossible = false; + let inertia = false; + let smoothEnd = false; + let modifierResult; + + if (this.dragging) { + if (options.drag.axis === 'x' ) { pointerSpeed = Math.abs(this.pointerDelta.client.vx); } + else if (options.drag.axis === 'y' ) { pointerSpeed = Math.abs(this.pointerDelta.client.vy); } + else /*options.drag.axis === 'xy'*/{ pointerSpeed = this.pointerDelta.client.speed; } + } + else { + pointerSpeed = this.pointerDelta.client.speed; + } + + // check if inertia should be started + inertiaPossible = (inertiaOptions && inertiaOptions.enabled + && this.prepared.name !== 'gesture' + && event !== inertiaStatus.startEvent); + + inertia = (inertiaPossible + && (now - this.curCoords.timeStamp) < 50 + && pointerSpeed > inertiaOptions.minSpeed + && pointerSpeed > inertiaOptions.endSpeed); + + // smoothEnd + if (inertiaPossible && !inertia) { + modifiers.resetStatuses(statuses); + + modifierResult = modifiers.setAll(this, page, statuses, true); + + if (modifierResult.shouldMove && modifierResult.locked) { + smoothEnd = true; + } + } + + if (inertia || smoothEnd) { + utils.copyCoords(inertiaStatus.upCoords, this.curCoords); + + this.pointers[0] = inertiaStatus.startEvent = + new InteractEvent(this, event, this.prepared.name, 'inertiastart', this.element); + + inertiaStatus.t0 = now; + + target.fire(inertiaStatus.startEvent); + + if (inertia) { + inertiaStatus.vx0 = this.pointerDelta.client.vx; + inertiaStatus.vy0 = this.pointerDelta.client.vy; + inertiaStatus.v0 = pointerSpeed; + + this.calcInertia(inertiaStatus); + + utils.extend(page, this.curCoords.page); + + page.x += inertiaStatus.xe; + page.y += inertiaStatus.ye; + + modifiers.resetStatuses(statuses); + + modifierResult = modifiers.setAll(this, page, statuses, true, true); + + inertiaStatus.modifiedXe += modifierResult.dx; + inertiaStatus.modifiedYe += modifierResult.dy; + + inertiaStatus.i = animationFrame.request(this.boundInertiaFrame); + } + else { + inertiaStatus.smoothEnd = true; + inertiaStatus.xe = modifierResult.dx; + inertiaStatus.ye = modifierResult.dy; + + inertiaStatus.sx = inertiaStatus.sy = 0; + + inertiaStatus.i = animationFrame.request(this.boundSmoothEndFrame); + } + + inertiaStatus.active = true; + return; + } + + for (let i = 0; i < modifiers.names.length; i++) { + // if the endOnly option is true for any modifier + if (modifiers[modifiers.names[i]].shouldDo(target, this.prepared.name, true, true)) { + // fire a move event at the snapped coordinates + this.pointerMove(pointer, event, eventTarget, curEventTarget, true); + break; + } + } + } + + if (this.interacting()) { + actions[this.prepared.name].end(this, event); + } + + this.stop(event); + } + + currentAction () { + return this._interacting? this.prepared.name: null; + } + + interacting () { + return this._interacting; + } + + stop (event) { + signals.fire('interaction-stop', { interaction: this }); + + if (this._interacting) { + signals.fire('interaction-stop-active', { interaction: this }); + + this.matches = []; + this.matchElements = []; + + const target = this.target; + + if (target.options.styleCursor) { + target._doc.documentElement.style.cursor = ''; + } + + // prevent Default only if were previously interacting + if (event && utils.isFunction(event.preventDefault)) { + this.checkAndPreventDefault(event, target, this.element); + } + + actions[this.prepared.name].stop(this, event); + } + + this.target = this.element = null; + + this.pointerIsDown = this._interacting = false; + this.prepared.name = this.prevEvent = null; + this.inertiaStatus.resumeDx = this.inertiaStatus.resumeDy = 0; + + modifiers.resetStatuses(this.modifierStatuses); + + // remove pointers if their ID isn't in this.pointerIds + for (let i = 0; i < this.pointers.length; i++) { + if (utils.indexOf(this.pointerIds, utils.getPointerId(this.pointers[i])) === -1) { + this.pointers.splice(i, 1); + } + } + } + + inertiaFrame () { + const inertiaStatus = this.inertiaStatus; + const options = this.target.options[this.prepared.name].inertia; + const lambda = options.resistance; + const t = new Date().getTime() / 1000 - inertiaStatus.t0; + + if (t < inertiaStatus.te) { + + const progress = 1 - (Math.exp(-lambda * t) - inertiaStatus.lambda_v0) / inertiaStatus.one_ve_v0; + + if (inertiaStatus.modifiedXe === inertiaStatus.xe && inertiaStatus.modifiedYe === inertiaStatus.ye) { + inertiaStatus.sx = inertiaStatus.xe * progress; + inertiaStatus.sy = inertiaStatus.ye * progress; + } + else { + const quadPoint = utils.getQuadraticCurvePoint(0, 0, + inertiaStatus.xe, + inertiaStatus.ye, + inertiaStatus.modifiedXe, + inertiaStatus.modifiedYe, + progress); + + inertiaStatus.sx = quadPoint.x; + inertiaStatus.sy = quadPoint.y; + } + + this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); + + inertiaStatus.i = animationFrame.request(this.boundInertiaFrame); + } + else { + inertiaStatus.ending = true; + + inertiaStatus.sx = inertiaStatus.modifiedXe; + inertiaStatus.sy = inertiaStatus.modifiedYe; + + this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); + + this.pointerEnd(inertiaStatus.startEvent, inertiaStatus.startEvent); + inertiaStatus.active = inertiaStatus.ending = false; + } + } + + smoothEndFrame () { + const inertiaStatus = this.inertiaStatus; + const t = new Date().getTime() - inertiaStatus.t0; + const duration = this.target.options[this.prepared.name].inertia.smoothEndDuration; + + if (t < duration) { + inertiaStatus.sx = utils.easeOutQuad(t, 0, inertiaStatus.xe, duration); + inertiaStatus.sy = utils.easeOutQuad(t, 0, inertiaStatus.ye, duration); + + this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); + + inertiaStatus.i = animationFrame.request(this.boundSmoothEndFrame); + } + else { + inertiaStatus.ending = true; + + inertiaStatus.sx = inertiaStatus.xe; + inertiaStatus.sy = inertiaStatus.ye; + + this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); + this.pointerEnd(inertiaStatus.startEvent, inertiaStatus.startEvent); + + inertiaStatus.smoothEnd = + inertiaStatus.active = inertiaStatus.ending = false; + } + } + + addPointer (pointer) { + const id = utils.getPointerId(pointer); + let index = this.mouse? 0 : utils.indexOf(this.pointerIds, id); + + if (index === -1) { + index = this.pointerIds.length; + } + + this.pointerIds[index] = id; + this.pointers[index] = pointer; + + return index; + } + + removePointer (pointer) { + const id = utils.getPointerId(pointer); + const index = this.mouse? 0 : utils.indexOf(this.pointerIds, id); + + if (index === -1) { return; } + + this.pointers .splice(index, 1); + this.pointerIds .splice(index, 1); + this.downTargets.splice(index, 1); + this.downTimes .splice(index, 1); + this.holdTimers .splice(index, 1); + } + + recordPointer (pointer) { + const index = this.mouse? 0: utils.indexOf(this.pointerIds, utils.getPointerId(pointer)); + + if (index === -1) { return; } + + this.pointers[index] = pointer; + } + + validateSelector (pointer, event, matches, matchElements) { + for (let i = 0, len = matches.length; i < len; i++) { + const match = matches[i]; + const matchElement = matchElements[i]; + const action = validateAction(match.getAction(pointer, event, this, matchElement), match); + + if (action && scope.withinInteractionLimit(match, matchElement, action)) { + this.target = match; + this.element = matchElement; + + return action; + } + } + } + + checkAndPreventDefault (event, interactable, element) { + if (!(interactable = interactable || this.target)) { return; } + + const options = interactable.options; + const prevent = options.preventDefault; + + if (prevent === 'auto' && element && !/^(input|select|textarea)$/i.test(event.target.nodeName)) { + // do not preventDefault on pointerdown if the prepared action is a drag + // and dragging can only start from a certain direction - this allows + // a touch to pan the viewport if a drag isn't in the right direction + if (/down|start/i.test(event.type) + && this.prepared.name === 'drag' && options.drag.axis !== 'xy') { + + return; + } + + // with manualStart, only preventDefault while interacting + if (options[this.prepared.name] && options[this.prepared.name].manualStart + && !this.interacting()) { + return; + } + + event.preventDefault(); + return; + } + + if (prevent === 'always') { + event.preventDefault(); + return; + } + } + + calcInertia (status) { + const inertiaOptions = this.target.options[this.prepared.name].inertia; + const lambda = inertiaOptions.resistance; + const inertiaDur = -Math.log(inertiaOptions.endSpeed / status.v0) / lambda; + + status.x0 = this.prevEvent.pageX; + status.y0 = this.prevEvent.pageY; + status.t0 = status.startEvent.timeStamp / 1000; + status.sx = status.sy = 0; + + status.modifiedXe = status.xe = (status.vx0 - inertiaDur) / lambda; + status.modifiedYe = status.ye = (status.vy0 - inertiaDur) / lambda; + status.te = inertiaDur; + + status.lambda_v0 = lambda / status.v0; + status.one_ve_v0 = 1 - inertiaOptions.endSpeed / status.v0; + } + + _updateEventTargets (target, currentTarget) { + this._eventTarget = target; + this._curEventTarget = currentTarget; + } +} + +// Check if the current target supports the action. +// If so, return the validated action. Otherwise, return null +function validateAction (action, interactable) { + if (utils.isObject(action) && interactable.options[action.name].enabled) { + return action; + } + + return null; +} + +for (let i = 0, len = methodNames.length; i < len; i++) { + const method = methodNames[i]; + + listeners[method] = doOnInteractions(method); +} + +function doOnInteractions (method) { + return (function (event) { + const eventTarget = utils.getActualElement(event.path ? event.path[0] : event.target); + const curEventTarget = utils.getActualElement(event.currentTarget); + const matches = []; // [ [pointer, interaction], ...] + + if (browser.supportsTouch && /touch/.test(event.type)) { + prevTouchTime = new Date().getTime(); + + for (const pointer of event.changedTouches) { + const interaction = finder.search(pointer, event.type, eventTarget); + + matches.push([pointer, interaction || new 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 < scope.interactions.length && !invalidPointer; i++) { + invalidPointer = !scope.interactions[i].mouse && scope.interactions[i].pointerIsDown; + } + + // try to ignore mouse events that are simulated by the browser + // after a touch event + invalidPointer = invalidPointer || (new Date().getTime() - prevTouchTime < 500); + } + + if (!invalidPointer) { + let interaction = finder.search(event, event.type, eventTarget); + + if (!interaction) { + + interaction = new Interaction(); + interaction.mouse = (/mouse/i.test(event.pointerType || event.type) + // MSPointerEvent.MSPOINTER_TYPE_MOUSE + || event.pointerType === 4); + } + + matches.push([event, interaction]); + } + } + + for (const [pointer, interaction] of matches) { + interaction._updateEventTargets(eventTarget, curEventTarget); + interaction[method](pointer, event, eventTarget, curEventTarget); + } + }); +} + +signals.on('interactable-new', function ({ interactable, win }) { + const element = interactable._element; + + if (utils.isElement(element, win)) { + if (scope.PointerEvent) { + events.add(element, browser.pEventTypes.down, listeners.pointerDown ); + events.add(element, browser.pEventTypes.move, listeners.pointerHover); + } + else { + events.add(element, 'mousedown' , listeners.pointerDown ); + events.add(element, 'mousemove' , listeners.pointerHover); + events.add(element, 'touchstart', listeners.pointerDown ); + events.add(element, 'touchmove' , listeners.pointerHover); + } + } +}); + +signals.on('interactable-unset', function ({ interactable, win }) { + const element = interactable._element; + + if (!interactable.selector && utils.isElement(element, win)) { + if (scope.PointerEvent) { + events.remove(element, browser.pEventTypes.down, listeners.pointerDown ); + events.remove(element, browser.pEventTypes.move, listeners.pointerHover); + } + else { + events.remove(element, 'mousedown' , listeners.pointerDown ); + events.remove(element, 'mousemove' , listeners.pointerHover); + events.remove(element, 'touchstart', listeners.pointerDown ); + events.remove(element, 'touchmove' , listeners.pointerHover); + } + } +}); + +signals.on('listen-to-document', function ({ doc, win }) { + const pEventTypes = browser.pEventTypes; + + // add delegate event listener + for (const eventType in scope.delegatedEvents) { + events.add(doc, eventType, events.delegateListener); + events.add(doc, eventType, events.delegateUseCapture, true); + } + + if (scope.PointerEvent) { + events.add(doc, pEventTypes.down , listeners.selectorDown ); + events.add(doc, pEventTypes.move , listeners.pointerMove ); + events.add(doc, pEventTypes.over , listeners.pointerOver ); + events.add(doc, pEventTypes.out , listeners.pointerOut ); + events.add(doc, pEventTypes.up , listeners.pointerUp ); + events.add(doc, pEventTypes.cancel, listeners.pointerCancel); + } + else { + events.add(doc, 'mousedown', listeners.selectorDown); + events.add(doc, 'mousemove', listeners.pointerMove ); + events.add(doc, 'mouseup' , listeners.pointerUp ); + events.add(doc, 'mouseover', listeners.pointerOver ); + events.add(doc, 'mouseout' , listeners.pointerOut ); + + events.add(doc, 'touchstart' , listeners.selectorDown ); + events.add(doc, 'touchmove' , listeners.pointerMove ); + events.add(doc, 'touchend' , listeners.pointerUp ); + events.add(doc, 'touchcancel', listeners.pointerCancel); + } + + events.add(win, 'blur', scope.endAllInteractions); + + try { + if (win.frameElement) { + const parentDoc = win.frameElement.ownerDocument; + const parentWindow = parentDoc.defaultView; + + events.add(parentDoc , 'mouseup' , listeners.pointerEnd); + events.add(parentDoc , 'touchend' , listeners.pointerEnd); + events.add(parentDoc , 'touchcancel' , listeners.pointerEnd); + events.add(parentDoc , 'pointerup' , listeners.pointerEnd); + events.add(parentDoc , 'MSPointerUp' , listeners.pointerEnd); + events.add(parentWindow, 'blur' , scope.endAllInteractions ); + } + } + catch (error) { + scope.windowParentError = error; + } + + // prevent native HTML5 drag on interact.js target elements + events.add(doc, 'dragstart', function (event) { + for (const interaction of scope.interactions) { + + if (interaction.element + && (interaction.element === event.target + || utils.nodeContains(interaction.element, event.target))) { + + interaction.checkAndPreventDefault(event, interaction.target, interaction.element); + return; + } + } + }); + + scope.documents.push(doc); + events.documents.push(doc); +}); + +signals.fire('listen-to-document', { + win: scope.window, + doc: scope.document, +}); + +Interaction.doOnInteractions = doOnInteractions; +Interaction.withinLimit = scope.withinInteractionLimit; + +module.exports = Interaction; diff --git a/src/actions/base.js b/src/actions/base.js new file mode 100644 index 000000000..09c0fcc4d --- /dev/null +++ b/src/actions/base.js @@ -0,0 +1,19 @@ +const actions = { + defaultChecker: function (pointer, event, interaction, element) { + const rect = this.getRect(element); + let action = null; + + for (const actionName of actions.names) { + action = actions[actionName].checker(pointer, event, this, element, interaction, rect); + + if (action) { + return action; + } + } + }, + + names: [], + methodDict: {}, +}; + +module.exports = actions; diff --git a/src/actions/drag.js b/src/actions/drag.js new file mode 100644 index 000000000..41f608f44 --- /dev/null +++ b/src/actions/drag.js @@ -0,0 +1,218 @@ +const base = require('./base'); +const drop = require('./drop'); +const scope = require('../scope'); +const utils = require('../utils'); +const browser = require('../utils/browser'); +const InteractEvent = require('../InteractEvent'); +const Interactable = require('../Interactable'); +const defaultOptions = require('../defaultOptions'); + +const drag = { + defaults: { + enabled : false, + manualStart : true, + max : Infinity, + maxPerElement: 1, + + snap : null, + restrict : null, + inertia : null, + autoScroll: null, + + axis: 'xy', + }, + + checker: function (pointer, event, interactable) { + return interactable.options.drag.enabled + ? { name: 'drag' } + : null; + }, + + getCursor: function () { + return 'move'; + }, + + beforeStart: function (interaction, pointer, event, eventTarget, curEventTarget, dx, dy) { + // check if a drag is in the correct axis + const absX = Math.abs(dx); + const absY = Math.abs(dy); + const targetAxis = interaction.target.options.drag.axis; + const axis = (absX > absY ? 'x' : absX < absY ? 'y' : 'xy'); + + // if the movement isn't in the axis of the interactable + if (axis !== 'xy' && targetAxis !== 'xy' && targetAxis !== axis) { + // cancel the prepared action + interaction.prepared.name = null; + + // then try to get a drag from another ineractable + + let element = eventTarget; + + // check element interactables + while (utils.isElement(element)) { + const elementInteractable = scope.interactables.get(element); + + if (elementInteractable + && elementInteractable !== interaction.target + && !elementInteractable.options.drag.manualStart + && elementInteractable.getAction(interaction.downPointer, interaction.downEvent, interaction, element).name === 'drag' + && checkAxis(axis, elementInteractable)) { + + interaction.prepared.name = 'drag'; + interaction.target = elementInteractable; + interaction.element = element; + break; + } + + element = utils.parentElement(element); + } + + // if there's no drag from element interactables, + // check the selector interactables + if (!interaction.prepared.name) { + + const getDraggable = function (interactable, selector, context) { + const elements = browser.useMatchesSelectorPolyfill + ? context.querySelectorAll(selector) + : undefined; + + if (interactable === interaction.target) { return; } + + if (scope.inContext(interactable, eventTarget) + && !interactable.options.drag.manualStart + && !scope.testIgnore(interactable, element, eventTarget) + && scope.testAllow(interactable, element, eventTarget) + && utils.matchesSelector(element, selector, elements) + && interactable.getAction(interaction.downPointer, interaction.downEvent, interaction, element).name === 'drag' + && checkAxis(axis, interactable) + && scope.withinInteractionLimit(interactable, element, 'drag')) { + + return interactable; + } + }; + + element = eventTarget; + + while (utils.isElement(element)) { + const selectorInteractable = scope.interactables.forEachSelector(getDraggable); + + if (selectorInteractable) { + interaction.prepared.name = 'drag'; + interaction.target = selectorInteractable; + interaction.element = element; + break; + } + + element = utils.parentElement(element); + } + } + } + }, + + start: function (interaction, event) { + const dragEvent = new InteractEvent(interaction, event, 'drag', 'start', interaction.element); + + interaction._interacting = true; + interaction.target.fire(dragEvent); + + drop.start(interaction, event, dragEvent); + + return dragEvent; + }, + + move: function (interaction, event) { + const dragEvent = new InteractEvent(interaction, event, 'drag', 'move', interaction.element); + + drop.move(interaction, event, dragEvent); + + return dragEvent; + }, + + end: function (interaction, event) { + const endEvent = new InteractEvent(interaction, event, 'drag', 'end', interaction.element); + + drop.end(interaction, event, endEvent); + + interaction.target.fire(endEvent); + }, + + stop: drop.stop, +}; + +function checkAxis (axis, interactable) { + if (!interactable) { return false; } + + const thisAxis = interactable.options.drag.axis; + + return (axis === 'xy' || thisAxis === 'xy' || thisAxis === axis); +} + +/*\ + * Interactable.draggable + [ method ] + * + * Gets or sets whether drag actions can be performed on the + * Interactable + * + = (boolean) Indicates if this can be the target of drag events + | var isDraggable = interact('ul li').draggable(); + * or + - options (boolean | object) #optional true/false or An object with event listeners to be fired on drag events (object makes the Interactable draggable) + = (object) This Interactable + | 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 + | axis: 'x' || 'y' || 'xy', + | + | // 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 + | }); +\*/ +Interactable.prototype.draggable = function (options) { + if (utils.isObject(options)) { + this.options.drag.enabled = options.enabled === false? false: true; + this.setPerAction('drag', options); + this.setOnEvents('drag', options); + + if (/^x$|^y$|^xy$/.test(options.axis)) { + this.options.drag.axis = options.axis; + } + else if (options.axis === null) { + delete this.options.drag.axis; + } + + return this; + } + + if (utils.isBool(options)) { + this.options.drag.enabled = options; + + return this; + } + + return this.options.drag; +}; + +base.drag = drag; +base.names.push('drag'); +utils.merge(scope.eventTypes, [ + 'dragstart', + 'dragmove', + 'draginertiastart', + 'dragend', +]); +base.methodDict.drag = 'draggable'; + +defaultOptions.drag = drag.defaults; + +module.exports = drag; diff --git a/src/actions/drop.js b/src/actions/drop.js new file mode 100644 index 000000000..dfd378728 --- /dev/null +++ b/src/actions/drop.js @@ -0,0 +1,457 @@ +const base = require('./base'); +const utils = require('../utils'); +const scope = require('../scope'); +const signals = require('../utils/signals'); +const Interactable = require('../Interactable'); +const defaultOptions = require('../defaultOptions'); + +const drop = { + defaults: { + enabled: false, + accept : null, + overlap: 'pointer', + }, + + start: function (interaction, event, dragEvent) { + // reset active dropzones + interaction.activeDrops.dropzones = []; + interaction.activeDrops.elements = []; + interaction.activeDrops.rects = []; + + if (!interaction.dynamicDrop) { + setActiveDrops(interaction, interaction.element); + } + + const dropEvents = getDropEvents(interaction, event, dragEvent); + + if (dropEvents.activate) { + fireActiveDrops(interaction, dropEvents.activate); + } + }, + + move: function (interaction, event, dragEvent) { + const draggableElement = interaction.element; + const dropOptions = getDrop(dragEvent, event, draggableElement); + + interaction.dropTarget = dropOptions.dropzone; + interaction.dropElement = dropOptions.element; + + const dropEvents = getDropEvents(interaction, event, dragEvent); + + interaction.target.fire(dragEvent); + + if (dropEvents.leave) { interaction.prevDropTarget.fire(dropEvents.leave); } + if (dropEvents.enter) { interaction.dropTarget.fire(dropEvents.enter); } + if (dropEvents.move ) { interaction.dropTarget.fire(dropEvents.move ); } + + interaction.prevDropTarget = interaction.dropTarget; + interaction.prevDropElement = interaction.dropElement; + }, + + end: function (interaction, event, endEvent) { + const draggableElement = interaction.element; + const dropResult = getDrop(endEvent, event, draggableElement); + + interaction.dropTarget = dropResult.dropzone; + interaction.dropElement = dropResult.element; + + const dropEvents = getDropEvents(interaction, event, endEvent); + + if (dropEvents.leave) { interaction.prevDropTarget.fire(dropEvents.leave); } + if (dropEvents.enter) { interaction.dropTarget.fire(dropEvents.enter); } + if (dropEvents.drop ) { interaction.dropTarget.fire(dropEvents.drop ); } + if (dropEvents.deactivate) { + fireActiveDrops(interaction, dropEvents.deactivate); + } + }, + + stop: function (interaction) { + interaction.activeDrops.dropzones = + interaction.activeDrops.elements = + interaction.activeDrops.rects = null; + }, +}; + +function collectDrops (interaction, element) { + const drops = []; + const elements = []; + + element = element || interaction.element; + + // collect all dropzones and their elements which qualify for a drop + for (const current of scope.interactables) { + if (!current.options.drop.enabled) { continue; } + + const accept = current.options.drop.accept; + + // test the draggable element against the dropzone's accept setting + if ((utils.isElement(accept) && accept !== element) + || (utils.isString(accept) + && !utils.matchesSelector(element, accept))) { + + continue; + } + + // query for new elements if necessary + const dropElements = current.selector + ? current._context.querySelectorAll(current.selector) + : [current._element]; + + for (let i = 0; i < dropElements.length; i++) { + const currentElement = dropElements[i]; + + if (currentElement !== element) { + drops.push(current); + elements.push(currentElement); + } + } + } + + return { + elements, + dropzones: drops, + }; +} + +function fireActiveDrops (interaction, event) { + let prevElement; + + // loop through all active dropzones and trigger event + for (let i = 0; i < interaction.activeDrops.dropzones.length; i++) { + const current = interaction.activeDrops.dropzones[i]; + const currentElement = interaction.activeDrops.elements [i]; + + // prevent trigger of duplicate events on same element + if (currentElement !== prevElement) { + // set current element as event target + event.target = currentElement; + current.fire(event); + } + prevElement = currentElement; + } +} + +// Collect a new set of possible drops and save them in activeDrops. +// setActiveDrops should always be called when a drag has just started or a +// drag event happens while dynamicDrop is true +function setActiveDrops (interaction, dragElement) { + // get dropzones and their elements that could receive the draggable + const possibleDrops = collectDrops(interaction, dragElement, true); + + interaction.activeDrops.dropzones = possibleDrops.dropzones; + interaction.activeDrops.elements = possibleDrops.elements; + interaction.activeDrops.rects = []; + + for (let i = 0; i < interaction.activeDrops.dropzones.length; i++) { + interaction.activeDrops.rects[i] = + interaction.activeDrops.dropzones[i].getRect(interaction.activeDrops.elements[i]); + } +} + +function getDrop (dragEvent, event, dragElement) { + const interaction = dragEvent.interaction; + const validDrops = []; + + if (scope.dynamicDrop) { + setActiveDrops(interaction, dragElement); + } + + // collect all dropzones and their elements which qualify for a drop + for (let j = 0; j < interaction.activeDrops.dropzones.length; j++) { + const current = interaction.activeDrops.dropzones[j]; + const currentElement = interaction.activeDrops.elements [j]; + const rect = interaction.activeDrops.rects [j]; + + validDrops.push(current.dropCheck(dragEvent, event, interaction.target, dragElement, currentElement, rect) + ? currentElement + : null); + } + + // get the most appropriate dropzone based on DOM depth and order + const dropIndex = utils.indexOfDeepestElement(validDrops); + + return { + dropzone: interaction.activeDrops.dropzones[dropIndex] || null, + element : interaction.activeDrops.elements [dropIndex] || null, + }; +} + +function getDropEvents (interaction, pointerEvent, dragEvent) { + const dropEvents = { + enter : null, + leave : null, + activate : null, + deactivate: null, + move : null, + drop : null, + }; + + if (interaction.dropElement !== interaction.prevDropElement) { + // if there was a prevDropTarget, create a dragleave event + if (interaction.prevDropTarget) { + dropEvents.leave = { + dragEvent, + interaction, + target : interaction.prevDropElement, + dropzone : interaction.prevDropTarget, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + timeStamp : dragEvent.timeStamp, + type : 'dragleave', + }; + + dragEvent.dragLeave = interaction.prevDropElement; + dragEvent.prevDropzone = interaction.prevDropTarget; + } + // if the dropTarget is not null, create a dragenter event + if (interaction.dropTarget) { + dropEvents.enter = { + dragEvent, + interaction, + target : interaction.dropElement, + dropzone : interaction.dropTarget, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + timeStamp : dragEvent.timeStamp, + type : 'dragenter', + }; + + dragEvent.dragEnter = interaction.dropElement; + dragEvent.dropzone = interaction.dropTarget; + } + } + + if (dragEvent.type === 'dragend' && interaction.dropTarget) { + dropEvents.drop = { + dragEvent, + interaction, + target : interaction.dropElement, + dropzone : interaction.dropTarget, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + timeStamp : dragEvent.timeStamp, + type : 'drop', + }; + + dragEvent.dropzone = interaction.dropTarget; + } + if (dragEvent.type === 'dragstart') { + dropEvents.activate = { + dragEvent, + interaction, + target : null, + dropzone : null, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + timeStamp : dragEvent.timeStamp, + type : 'dropactivate', + }; + } + if (dragEvent.type === 'dragend') { + dropEvents.deactivate = { + dragEvent, + interaction, + target : null, + dropzone : null, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + timeStamp : dragEvent.timeStamp, + type : 'dropdeactivate', + }; + } + if (dragEvent.type === 'dragmove' && interaction.dropTarget) { + dropEvents.move = { + dragEvent, + interaction, + target : interaction.dropElement, + dropzone : interaction.dropTarget, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + dragmove : dragEvent, + timeStamp : dragEvent.timeStamp, + type : 'dropmove', + }; + dragEvent.dropzone = interaction.dropTarget; + } + + return dropEvents; +} + +/*\ + * Interactable.dropzone + [ method ] + * + * Returns or sets whether elements can be dropped onto this + * Interactable 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. + * + | 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'); + | } + * + * + - options (boolean | object | null) #optional The new value to be set. + | interact('.drop').dropzone({ + | accept: '.can-drop' || document.getElementById('single-drop'), + | overlap: 'pointer' || 'center' || zeroToOne + | } + = (boolean | object) The current setting or this Interactable +\*/ +Interactable.prototype.dropzone = function (options) { + if (utils.isObject(options)) { + this.options.drop.enabled = options.enabled === false? false: true; + + if (utils.isFunction(options.ondrop) ) { this.ondrop = options.ondrop ; } + if (utils.isFunction(options.ondropactivate) ) { this.ondropactivate = options.ondropactivate ; } + if (utils.isFunction(options.ondropdeactivate)) { this.ondropdeactivate = options.ondropdeactivate; } + if (utils.isFunction(options.ondragenter) ) { this.ondragenter = options.ondragenter ; } + if (utils.isFunction(options.ondragleave) ) { this.ondragleave = options.ondragleave ; } + if (utils.isFunction(options.ondropmove) ) { this.ondropmove = options.ondropmove ; } + + if (/^(pointer|center)$/.test(options.overlap)) { + this.options.drop.overlap = options.overlap; + } + else if (utils.isNumber(options.overlap)) { + this.options.drop.overlap = Math.max(Math.min(1, options.overlap), 0); + } + if ('accept' in options) { + this.options.drop.accept = options.accept; + } + if ('checker' in options) { + this.options.drop.checker = options.checker; + } + + + return this; + } + + if (utils.isBool(options)) { + this.options.drop.enabled = options; + + return this; + } + + return this.options.drop; +}; + +Interactable.prototype.dropCheck = function (dragEvent, event, draggable, draggableElement, dropElement, rect) { + let dropped = false; + + // if the dropzone has no rect (eg. display: none) + // call the custom dropChecker or just return false + if (!(rect = rect || this.getRect(dropElement))) { + return (this.options.drop.checker + ? this.options.drop.checker(dragEvent, event, dropped, this, dropElement, draggable, draggableElement) + : false); + } + + const dropOverlap = this.options.drop.overlap; + + if (dropOverlap === 'pointer') { + const origin = utils.getOriginXY(draggable, draggableElement); + const page = utils.getPageXY(dragEvent); + let horizontal; + let vertical; + + page.x += origin.x; + page.y += origin.y; + + horizontal = (page.x > rect.left) && (page.x < rect.right); + vertical = (page.y > rect.top ) && (page.y < rect.bottom); + + dropped = horizontal && vertical; + } + + const dragRect = draggable.getRect(draggableElement); + + if (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 (utils.isNumber(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 (this.options.drop.checker) { + dropped = this.options.drop.checker(dragEvent, event, dropped, this, dropElement, draggable, draggableElement); + } + + return dropped; +}; + +signals.on('interactable-unset', function ({ interactable }) { + interactable.dropzone(false); +}); + +signals.on('interaction-new', function (interaction) { + interaction.dropTarget = null; // the dropzone a drag target might be dropped into + interaction.dropElement = null; // the element at the time of checking + interaction.prevDropTarget = null; // the dropzone that was recently dragged away from + interaction.prevDropElement = null; // the element at the time of checking + + interaction.activeDrops = { + dropzones: [], // the dropzones that are mentioned below + elements : [], // elements of dropzones that accept the target draggable + rects : [], // the rects of the elements mentioned above + }; + +}); + +signals.on('interaction-stop', function ({ interaction }) { + interaction.dropTarget = interaction.dropElement = + interaction.prevDropTarget = interaction.prevDropElement = null; +}); + +utils.merge(scope.eventTypes, [ + 'dragenter', + 'dragleave', + 'dropactivate', + 'dropdeactivate', + 'dropmove', + 'drop', +]); +base.methodDict.drop = 'dropzone'; + +defaultOptions.drop = drop.defaults; + +module.exports = drop; diff --git a/src/actions/gesture.js b/src/actions/gesture.js new file mode 100644 index 000000000..d125b1632 --- /dev/null +++ b/src/actions/gesture.js @@ -0,0 +1,187 @@ +const base = require('./base'); +const utils = require('../utils'); +const InteractEvent = require('../InteractEvent'); +const Interactable = require('../Interactable'); +const scope = require('../scope'); +const signals = require('../utils/signals'); +const defaultOptions = require('../defaultOptions'); + +const gesture = { + defaults: { + manualStart : false, + enabled : false, + max : Infinity, + maxPerElement: 1, + + restrict: null, + }, + + checker: function (pointer, event, interactable, element, interaction) { + if (interaction.pointerIds.length >= 2) { + return { name: 'gesture' }; + } + + return null; + }, + + getCursor: function () { + return ''; + }, + + beforeStart: utils.blank, + + start: function (interaction, event) { + const gestureEvent = new InteractEvent(interaction, event, 'gesture', 'start', interaction.element); + + gestureEvent.ds = 0; + + interaction.gesture.startDistance = interaction.gesture.prevDistance = gestureEvent.distance; + interaction.gesture.startAngle = interaction.gesture.prevAngle = gestureEvent.angle; + interaction.gesture.scale = 1; + + interaction._interacting = true; + + interaction.target.fire(gestureEvent); + + return gestureEvent; + }, + + move: function (interaction, event) { + if (!interaction.pointerIds.length) { + return interaction.prevEvent; + } + + let gestureEvent; + + gestureEvent = new InteractEvent(interaction, event, 'gesture', 'move', interaction.element); + gestureEvent.ds = gestureEvent.scale - interaction.gesture.scale; + + interaction.target.fire(gestureEvent); + + interaction.gesture.prevAngle = gestureEvent.angle; + interaction.gesture.prevDistance = gestureEvent.distance; + + if (gestureEvent.scale !== Infinity + && gestureEvent.scale !== null + && gestureEvent.scale !== undefined + && !isNaN(gestureEvent.scale)) { + + interaction.gesture.scale = gestureEvent.scale; + } + + return gestureEvent; + }, + + end: function (interaction, event) { + const endEvent = new InteractEvent(interaction, event, 'gesture', 'end', interaction.element); + + interaction.target.fire(endEvent); + }, + + stop: utils.blank, +}; + +/*\ + * Interactable.gesturable + [ method ] + * + * Gets or sets whether multitouch gestures can be performed on the + * Interactable's element + * + = (boolean) Indicates if this can be the target of gesture events + | var isGestureable = interact(element).gesturable(); + * or + - options (boolean | object) #optional true/false or An object with event listeners to be fired on gesture events (makes the Interactable gesturable) + = (object) this Interactable + | interact(element).gesturable({ + | onstart: function (event) {}, + | onmove : function (event) {}, + | onend : function (event) {}, + | + | // limit multiple gestures. + | // See the explanation in @Interactable.draggable example + | max: Infinity, + | maxPerElement: 1, + | }); +\*/ +Interactable.prototype.gesturable = function (options) { + if (utils.isObject(options)) { + this.options.gesture.enabled = options.enabled === false? false: true; + this.setPerAction('gesture', options); + this.setOnEvents('gesture', options); + + return this; + } + + if (utils.isBool(options)) { + this.options.gesture.enabled = options; + + return this; + } + + return this.options.gesture; +}; + +signals.on('interactevent-gesture', function (arg) { + if (arg.action !== 'gesture') { return; } + + const { interaction, iEvent, starting, ending, deltaSource } = arg; + const pointers = interaction.pointers; + + iEvent.touches = [pointers[0], pointers[1]]; + + if (starting) { + iEvent.distance = utils.touchDistance(pointers, deltaSource); + iEvent.box = utils.touchBBox(pointers); + iEvent.scale = 1; + iEvent.ds = 0; + iEvent.angle = utils.touchAngle(pointers, undefined, deltaSource); + iEvent.da = 0; + } + else if (ending || event instanceof InteractEvent) { + iEvent.distance = interaction.prevEvent.distance; + iEvent.box = interaction.prevEvent.box; + iEvent.scale = interaction.prevEvent.scale; + iEvent.ds = iEvent.scale - 1; + iEvent.angle = interaction.prevEvent.angle; + iEvent.da = iEvent.angle - interaction.gesture.startAngle; + } + else { + iEvent.distance = utils.touchDistance(pointers, deltaSource); + iEvent.box = utils.touchBBox(pointers); + iEvent.scale = iEvent.distance / interaction.gesture.startDistance; + iEvent.angle = utils.touchAngle(pointers, interaction.gesture.prevAngle, deltaSource); + + iEvent.ds = iEvent.scale - interaction.gesture.prevScale; + iEvent.da = iEvent.angle - interaction.gesture.prevAngle; + } +}); + +signals.on('interaction-new', function (interaction) { + interaction.gesture = { + start: { x: 0, y: 0 }, + + startDistance: 0, // distance between two touches of touchStart + prevDistance : 0, + distance : 0, + + scale: 1, // gesture.distance / gesture.startDistance + + startAngle: 0, // angle of line joining two touches + prevAngle : 0, // angle of the previous gesture event + }; +}); + +base.gesture = gesture; +base.names.push('gesture'); +utils.merge(scope.eventTypes, [ + 'gesturestart', + 'gesturemove', + 'gestureinertiastart', + 'gestureend', +]); +base.methodDict.gesture = 'gesturable'; + +defaultOptions.gesture = gesture.defaults; + +module.exports = gesture; diff --git a/src/actions/resize.js b/src/actions/resize.js new file mode 100644 index 000000000..50eacca11 --- /dev/null +++ b/src/actions/resize.js @@ -0,0 +1,449 @@ +const base = require('./base'); +const utils = require('../utils'); +const browser = require('../utils/browser'); +const signals = require('../utils/signals'); +const scope = require('../scope'); +const InteractEvent = require('../InteractEvent'); +const Interactable = require('../Interactable'); +const defaultOptions = require('../defaultOptions'); + +const resize = { + defaults: { + enabled : false, + manualStart : false, + max : Infinity, + maxPerElement: 1, + + snap : null, + restrict : null, + inertia : null, + autoScroll: null, + + 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', + }, + + checker: function (pointer, event, interactable, element, interaction, rect) { + if (!rect) { return null; } + + const page = utils.extend({}, interaction.curCoords.page); + const options = interactable.options; + + if (options.resize.enabled) { + const resizeOptions = options.resize; + const resizeEdges = { left: false, right: false, top: false, bottom: false }; + + // if using resize.edges + if (utils.isObject(resizeOptions.edges)) { + for (const edge in resizeEdges) { + resizeEdges[edge] = checkResizeEdge(edge, + resizeOptions.edges[edge], + page, + interaction._eventTarget, + element, + rect, + resizeOptions.margin || scope.margin); + } + + resizeEdges.left = resizeEdges.left && !resizeEdges.right; + resizeEdges.top = resizeEdges.top && !resizeEdges.bottom; + + if (resizeEdges.left || resizeEdges.right || resizeEdges.top || resizeEdges.bottom) { + return { + name: 'resize', + edges: resizeEdges, + }; + } + } + else { + const right = options.resize.axis !== 'y' && page.x > (rect.right - scope.margin); + const bottom = options.resize.axis !== 'x' && page.y > (rect.bottom - scope.margin); + + if (right || bottom) { + return { + name: 'resize', + axes: (right? 'x' : '') + (bottom? 'y' : ''), + }; + } + } + } + + return null; + }, + + cursors: (browser.isIe9OrOlder ? { + 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', + }), + + getCursor: function (action) { + if (action.axis) { + return resize.cursors[action.name + action.axis]; + } + else if (action.edges) { + let cursorKey = ''; + const edgeNames = ['top', 'bottom', 'left', 'right']; + + for (let i = 0; i < 4; i++) { + if (action.edges[edgeNames[i]]) { + cursorKey += edgeNames[i]; + } + } + + return resize.cursors[cursorKey]; + } + }, + + beforeStart: utils.blank, + + start: function (interaction, event) { + const resizeEvent = new InteractEvent(interaction, event, 'resize', 'start', interaction.element); + + if (interaction.prepared.edges) { + const startRect = interaction.target.getRect(interaction.element); + const resizeOptions = interaction.target.options.resize; + + /* + * When using the `resizable.square` or `resizable.preserveAspectRatio` options, resizing from one edge + * will affect another. E.g. with `resizable.square`, resizing to make the right edge larger will make + * the bottom edge larger by the same amount. We call these 'linked' edges. Any linked edges will depend + * on the active edges and the edge being interacted with. + */ + if (resizeOptions.square || resizeOptions.preserveAspectRatio) { + const linkedEdges = utils.extend({}, interaction.prepared.edges); + + linkedEdges.top = linkedEdges.top || (linkedEdges.left && !linkedEdges.bottom); + linkedEdges.left = linkedEdges.left || (linkedEdges.top && !linkedEdges.right ); + linkedEdges.bottom = linkedEdges.bottom || (linkedEdges.right && !linkedEdges.top ); + linkedEdges.right = linkedEdges.right || (linkedEdges.bottom && !linkedEdges.left ); + + interaction.prepared._linkedEdges = linkedEdges; + } + else { + interaction.prepared._linkedEdges = null; + } + + // if using `resizable.preserveAspectRatio` option, record aspect ratio at the start of the resize + if (resizeOptions.preserveAspectRatio) { + interaction.resizeStartAspectRatio = startRect.width / startRect.height; + } + + interaction.resizeRects = { + start : startRect, + current : utils.extend({}, startRect), + restricted: utils.extend({}, startRect), + previous : utils.extend({}, startRect), + delta : { + left: 0, right : 0, width : 0, + top : 0, bottom: 0, height: 0, + }, + }; + + resizeEvent.rect = interaction.resizeRects.restricted; + resizeEvent.deltaRect = interaction.resizeRects.delta; + } + + interaction.target.fire(resizeEvent); + + interaction._interacting = true; + + return resizeEvent; + }, + + move: function (interaction, event) { + const resizeEvent = new InteractEvent(interaction, event, 'resize', 'move', interaction.element); + const resizeOptions = interaction.target.options.resize; + const invert = resizeOptions.invert; + const invertible = invert === 'reposition' || invert === 'negate'; + + let edges = interaction.prepared.edges; + + if (edges) { + const start = interaction.resizeRects.start; + const current = interaction.resizeRects.current; + const restricted = interaction.resizeRects.restricted; + const delta = interaction.resizeRects.delta; + const previous = utils.extend(interaction.resizeRects.previous, restricted); + const originalEdges = edges; + + let dx = resizeEvent.dx; + let dy = resizeEvent.dy; + + // `resize.preserveAspectRatio` takes precedence over `resize.square` + if (resizeOptions.preserveAspectRatio) { + const resizeStartAspectRatio = interaction.resizeStartAspectRatio; + + edges = interaction.prepared._linkedEdges; + + if ((originalEdges.left && originalEdges.bottom) + || (originalEdges.right && originalEdges.top)) { + dy = -dx / resizeStartAspectRatio; + } + else if (originalEdges.left || originalEdges.right) { dy = dx / resizeStartAspectRatio; } + else if (originalEdges.top || originalEdges.bottom) { dx = dy * resizeStartAspectRatio; } + } + else if (resizeOptions.square) { + edges = interaction.prepared._linkedEdges; + + if ((originalEdges.left && originalEdges.bottom) + || (originalEdges.right && originalEdges.top)) { + dy = -dx; + } + else if (originalEdges.left || originalEdges.right) { dy = dx; } + else if (originalEdges.top || originalEdges.bottom) { dx = dy; } + } + + // update the 'current' rect without modifications + if (edges.top ) { current.top += dy; } + if (edges.bottom) { current.bottom += dy; } + if (edges.left ) { current.left += dx; } + if (edges.right ) { current.right += dx; } + + if (invertible) { + // if invertible, copy the current rect + utils.extend(restricted, current); + + if (invert === 'reposition') { + // swap edge values if necessary to keep width/height positive + let swap; + + if (restricted.top > restricted.bottom) { + swap = restricted.top; + + restricted.top = restricted.bottom; + restricted.bottom = swap; + } + if (restricted.left > restricted.right) { + swap = restricted.left; + + restricted.left = restricted.right; + restricted.right = swap; + } + } + } + else { + // if not invertible, restrict to minimum of 0x0 rect + restricted.top = Math.min(current.top, start.bottom); + restricted.bottom = Math.max(current.bottom, start.top); + restricted.left = Math.min(current.left, start.right); + restricted.right = Math.max(current.right, start.left); + } + + restricted.width = restricted.right - restricted.left; + restricted.height = restricted.bottom - restricted.top ; + + for (const edge in restricted) { + delta[edge] = restricted[edge] - previous[edge]; + } + + resizeEvent.edges = interaction.prepared.edges; + resizeEvent.rect = restricted; + resizeEvent.deltaRect = delta; + } + + interaction.target.fire(resizeEvent); + + return resizeEvent; + }, + + end: function (interaction, event) { + const endEvent = new InteractEvent(interaction, event, 'resize', 'end', interaction.element); + + interaction.target.fire(endEvent); + }, + + stop: utils.blank, +}; + +/*\ + * Interactable.resizable + [ method ] + * + * Gets or sets whether resize actions can be performed on the + * Interactable + * + = (boolean) Indicates if this can be the target of resize elements + | var isResizeable = interact('input[type=text]').resizable(); + * or + - options (boolean | object) #optional true/false or An object with event listeners to be fired on resize events (object makes the Interactable resizable) + = (object) This Interactable + | 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 @Interactable.draggable example + | max: Infinity, + | maxPerElement: 1, + | }); + \*/ +Interactable.prototype.resizable = function (options) { + if (utils.isObject(options)) { + this.options.resize.enabled = options.enabled === false? false: true; + this.setPerAction('resize', options); + this.setOnEvents('resize', options); + + if (/^x$|^y$|^xy$/.test(options.axis)) { + this.options.resize.axis = options.axis; + } + else if (options.axis === null) { + this.options.resize.axis = scope.defaultOptions.resize.axis; + } + + if (utils.isBool(options.preserveAspectRatio)) { + this.options.resize.preserveAspectRatio = options.preserveAspectRatio; + } + else if (utils.isBool(options.square)) { + this.options.resize.square = options.square; + } + + return this; + } + if (utils.isBool(options)) { + this.options.resize.enabled = options; + + return this; + } + return this.options.resize; +}; + +function checkResizeEdge (name, value, page, element, interactableElement, rect, margin) { + // 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 = utils.isNumber(rect.width )? rect.width : rect.right - rect.left; + const height = utils.isNumber(rect.height)? rect.height : rect.bottom - rect.top ; + + 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' ) { return page.x < ((width >= 0? rect.left: rect.right ) + margin); } + if (name === 'top' ) { return page.y < ((height >= 0? rect.top : rect.bottom) + 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 (!utils.isElement(element)) { return false; } + + return utils.isElement(value) + // the value is an element to use as a resize handle + ? value === element + // otherwise check if element matches value as selector + : utils.matchesUpTo(element, value, interactableElement); +} + +signals.on('interaction-new', function (interaction) { + interaction.resizeAxes = 'xy'; +}); + +signals.on('interactevent-resize', function ({ interaction, iEvent }) { + if (!interaction.resizeAxes) { return; } + + const options = interaction.target.options; + + if (options.resize.square) { + if (interaction.resizeAxes === 'y') { + iEvent.dx = iEvent.dy; + } + else { + iEvent.dy = iEvent.dx; + } + iEvent.axes = 'xy'; + } + else { + iEvent.axes = interaction.resizeAxes; + + if (interaction.resizeAxes === 'x') { + iEvent.dy = 0; + } + else if (interaction.resizeAxes === 'y') { + iEvent.dx = 0; + } + } +}); + +base.resize = resize; +base.names.push('resize'); +utils.merge(scope.eventTypes, [ + 'resizestart', + 'resizemove', + 'resizeinertiastart', + 'resizeend', +]); +base.methodDict.resize = 'resizable'; + +defaultOptions.resize = resize.defaults; + +module.exports = resize; diff --git a/src/autoScroll.js b/src/autoScroll.js new file mode 100644 index 000000000..3fa2077ce --- /dev/null +++ b/src/autoScroll.js @@ -0,0 +1,124 @@ +const raf = require('./utils/raf'); +const getWindow = require('./utils/window').getWindow; +const isWindow = require('./utils/isType').isWindow; +const domUtils = require('./utils/domUtils'); +const signals = require('./utils/signals'); +const defaultOptions = require('./defaultOptions'); + +const autoScroll = { + defaults: { + enabled : false, + container: null, // the item that is scrolled (Window or HTMLElement) + margin : 60, + speed : 300, // the scroll speed in pixels per second + }, + + interaction: null, + i: null, // the handle returned by window.setInterval + x: 0, y: 0, // Direction each pulse is to scroll in + + isScrolling: false, + prevTime: 0, + + start: function (interaction) { + autoScroll.isScrolling = true; + raf.cancel(autoScroll.i); + + autoScroll.interaction = interaction; + autoScroll.prevTime = new Date().getTime(); + autoScroll.i = raf.request(autoScroll.scroll); + }, + + stop: function () { + autoScroll.isScrolling = false; + raf.cancel(autoScroll.i); + }, + + // scroll the window by the values in scroll.x/y + scroll: function () { + const options = autoScroll.interaction.target.options[autoScroll.interaction.prepared.name].autoScroll; + const container = options.container || getWindow(autoScroll.interaction.element); + const now = new Date().getTime(); + // change in time in seconds + const dt = (now - autoScroll.prevTime) / 1000; + // displacement + const s = options.speed * dt; + + if (s >= 1) { + if (isWindow(container)) { + container.scrollBy(autoScroll.x * s, autoScroll.y * s); + } + else if (container) { + container.scrollLeft += autoScroll.x * s; + container.scrollTop += autoScroll.y * s; + } + + autoScroll.prevTime = now; + } + + if (autoScroll.isScrolling) { + raf.cancel(autoScroll.i); + autoScroll.i = raf.request(autoScroll.scroll); + } + }, + check: function (interactable, actionName) { + const options = interactable.options; + + return options[actionName].autoScroll && options[actionName].autoScroll.enabled; + }, + onInteractionMove: function ({ interaction, pointer }) { + if (!(interaction.interacting() + && autoScroll.check(interaction.target, interaction.prepared.name))) { + return; + } + + if (interaction.inertiaStatus.active) { + autoScroll.x = autoScroll.y = 0; + return; + } + + let top; + let right; + let bottom; + let left; + + const options = interaction.target.options[interaction.prepared.name].autoScroll; + const container = options.container || getWindow(interaction.element); + + if (isWindow(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); + } + }, +}; + +signals.on('interaction-stop-active', function () { + autoScroll.stop(); +}); + +signals.on('interaction-move-done', autoScroll.onInteractionMove); + +defaultOptions.perAction.autoScroll = autoScroll.defaults; + +module.exports = autoScroll; diff --git a/src/defaultOptions.js b/src/defaultOptions.js new file mode 100644 index 000000000..1ef145b4f --- /dev/null +++ b/src/defaultOptions.js @@ -0,0 +1,31 @@ +module.exports = { + base: { + accept : null, + actionChecker : null, + styleCursor : true, + preventDefault: 'auto', + origin : { x: 0, y: 0 }, + deltaSource : 'page', + allowFrom : null, + ignoreFrom : null, + checker : null, + }, + + perAction: { + manualStart: false, + max: Infinity, + maxPerElement: 1, + + 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 + zeroResumeDelta : true, // if an action is resumed after launch, set dx/dy to 0 + smoothEndDuration: 300, // animate to snap/restrict endOnly if there's no inertia + }, + }, + + _holdDuration: 600, +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 000000000..f1c6f094d --- /dev/null +++ b/src/index.js @@ -0,0 +1,23 @@ +// browser entry point + +// Legacy browser support +require('./legacyBrowsers'); + +// actions +require('./actions/resize'); +require('./actions/drag'); +require('./actions/gesture'); + +// autoScroll +require('./autoScroll'); + +// pointerEvents +require('./pointerEvents'); + +// modifiers +require('./modifiers/snap'); +require('./modifiers/restrict'); + +require('./Interaction.js'); + +module.exports = require('./interact'); diff --git a/src/interact.js b/src/interact.js new file mode 100644 index 000000000..046c20523 --- /dev/null +++ b/src/interact.js @@ -0,0 +1,368 @@ +/** + * interact.js v1.2.5 + * + * Copyright (c) 2012-2015 Taye Adeyemi + * Open source under the MIT License. + * https://raw.github.com/taye/interact.js/master/LICENSE + */ + +const browser = require('./utils/browser'); +const events = require('./utils/events'); +const utils = require('./utils'); +const scope = require('./scope'); +const Interactable = require('./Interactable'); + +scope.dynamicDrop = false; + +// Less Precision with touch input +scope.margin = browser.supportsTouch || browser.supportsPointerEvent? 20: 10; + +scope.pointerMoveTolerance = 1; + +// Allow this many interactions to happen simultaneously +scope.maxInteractions = Infinity; + +// because Webkit and Opera still use 'mousewheel' event type +scope.wheelEvent = 'onmousewheel' in scope.document? 'mousewheel': 'wheel'; + +scope.globalEvents = {}; + +scope.inContext = function (interactable, element) { + return (interactable._context === element.ownerDocument + || utils.nodeContains(interactable._context, element)); +}; + +scope.testIgnore = function (interactable, interactableElement, element) { + const ignoreFrom = interactable.options.ignoreFrom; + + if (!ignoreFrom || !utils.isElement(element)) { return false; } + + if (utils.isString(ignoreFrom)) { + return utils.matchesUpTo(element, ignoreFrom, interactableElement); + } + else if (utils.isElement(ignoreFrom)) { + return utils.nodeContains(ignoreFrom, element); + } + + return false; +}; + +scope.testAllow = function (interactable, interactableElement, element) { + const allowFrom = interactable.options.allowFrom; + + if (!allowFrom) { return true; } + + if (!utils.isElement(element)) { return false; } + + if (utils.isString(allowFrom)) { + return utils.matchesUpTo(element, allowFrom, interactableElement); + } + else if (utils.isElement(allowFrom)) { + return utils.nodeContains(allowFrom, element); + } + + return false; +}; + +scope.interactables.indexOfElement = function indexOfElement (element, context) { + context = context || scope.document; + + for (let i = 0; i < this.length; i++) { + const interactable = this[i]; + + if ((interactable.selector === element + && (interactable._context === context)) + || (!interactable.selector && interactable._element === element)) { + + return i; + } + } + return -1; +}; + +scope.interactables.get = function interactableGet (element, options) { + return this[this.indexOfElement(element, options && options.context)]; +}; + +scope.interactables.forEachSelector = function (callback) { + for (let i = 0; i < this.length; i++) { + const interactable = this[i]; + + if (!interactable.selector) { + continue; + } + + const ret = callback(interactable, interactable.selector, interactable._context, i, this); + + if (ret !== undefined) { + return ret; + } + } +}; + +/*\ + * interact + [ method ] + * + * 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. + * + - element (Element | string) The HTML or SVG Element to interact with or CSS selector + = (object) An @Interactable + * + > Usage + | interact(document.getElementById('draggable')).draggable(true); + | + | var rectables = interact('rect'); + | rectables + | .gesturable(true) + | .on('gesturemove', function (event) { + | // something cool... + | }) + | .autoScroll(true); +\*/ +function interact (element, options) { + return scope.interactables.get(element, options) || new Interactable(element, options); +} + +/*\ + * interact.isSet + [ method ] + * + * Check if an element has been set + - element (Element) The Element being searched for + = (boolean) Indicates if the element or CSS selector was previously passed to interact +\*/ +interact.isSet = function (element, options) { + return scope.interactables.indexOfElement(element, options && options.context) !== -1; +}; + +/*\ + * interact.on + [ method ] + * + * Adds a global listener for an InteractEvent or adds a DOM event to + * `document` + * + - type (string | array | object) The types of events to listen for + - listener (function) The function event (s) + - useCapture (boolean) #optional useCapture flag for addEventListener + = (object) interact +\*/ +interact.on = function (type, listener, useCapture) { + if (utils.isString(type) && type.search(' ') !== -1) { + type = type.trim().split(/ +/); + } + + if (utils.isArray(type)) { + for (const eventType of type) { + interact.on(eventType, listener, useCapture); + } + + return interact; + } + + if (utils.isObject(type)) { + for (const prop in type) { + interact.on(prop, type[prop], listener); + } + + return interact; + } + + // if it is an InteractEvent type, add listener to globalEvents + if (utils.contains(scope.eventTypes, type)) { + // if this type of event was never bound + if (!scope.globalEvents[type]) { + scope.globalEvents[type] = [listener]; + } + else { + scope.globalEvents[type].push(listener); + } + } + // If non InteractEvent type, addEventListener to document + else { + events.add(scope.document, type, listener, useCapture); + } + + return interact; +}; + +/*\ + * interact.off + [ method ] + * + * Removes a global InteractEvent listener or DOM event from `document` + * + - type (string | array | object) The types of events that were listened for + - listener (function) The listener function to be removed + - useCapture (boolean) #optional useCapture flag for removeEventListener + = (object) interact + \*/ +interact.off = function (type, listener, useCapture) { + if (utils.isString(type) && type.search(' ') !== -1) { + type = type.trim().split(/ +/); + } + + if (utils.isArray(type)) { + for (const eventType of type) { + interact.off(eventType, listener, useCapture); + } + + return interact; + } + + if (utils.isObject(type)) { + for (const prop in type) { + interact.off(prop, type[prop], listener); + } + + return interact; + } + + if (!utils.contains(scope.eventTypes, type)) { + events.remove(scope.document, type, listener, useCapture); + } + else { + let index; + + if (type in scope.globalEvents + && (index = utils.indexOf(scope.globalEvents[type], listener)) !== -1) { + scope.globalEvents[type].splice(index, 1); + } + } + + return interact; +}; + +/*\ + * interact.debug + [ method ] + * + * Returns an object which exposes internal data + = (object) An object with properties that outline the current state and expose internal functions and variables +\*/ +interact.debug = function () { + return scope; +}; + +// expose the functions used to calculate multi-touch properties +interact.getPointerAverage = utils.pointerAverage; +interact.getTouchBBox = utils.touchBBox; +interact.getTouchDistance = utils.touchDistance; +interact.getTouchAngle = utils.touchAngle; + +interact.getElementRect = utils.getElementRect; +interact.getElementClientRect = utils.getElementClientRect; +interact.matchesSelector = utils.matchesSelector; +interact.closest = utils.closest; + +/*\ + * interact.supportsTouch + [ method ] + * + = (boolean) Whether or not the browser supports touch input +\*/ +interact.supportsTouch = function () { + return browser.supportsTouch; +}; + +/*\ + * interact.supportsPointerEvent + [ method ] + * + = (boolean) Whether or not the browser supports PointerEvents +\*/ +interact.supportsPointerEvent = function () { + return browser.supportsPointerEvent; +}; + +/*\ + * interact.stop + [ method ] + * + * Cancels all interactions (end events are not fired) + * + - event (Event) An event on which to call preventDefault() + = (object) interact +\*/ +interact.stop = function (event) { + for (let i = scope.interactions.length - 1; i >= 0; i--) { + scope.interactions[i].stop(event); + } + + return interact; +}; + +/*\ + * interact.dynamicDrop + [ method ] + * + * Returns or sets whether the dimensions of dropzone elements are + * calculated on every dragmove or only on dragstart for the default + * dropChecker + * + - newValue (boolean) #optional True to check on each move. False to check only before start + = (boolean | interact) The current setting or interact +\*/ +interact.dynamicDrop = function (newValue) { + if (utils.isBool(newValue)) { + //if (dragging && dynamicDrop !== newValue && !newValue) { + //calcRects(dropzones); + //} + + scope.dynamicDrop = newValue; + + return interact; + } + return scope.dynamicDrop; +}; + +/*\ + * interact.pointerMoveTolerance + [ method ] + * Returns or sets the distance the pointer must be moved before an action + * sequence occurs. This also affects tolerance for tap events. + * + - newValue (number) #optional The movement from the start position must be greater than this value + = (number | Interactable) The current setting or interact +\*/ +interact.pointerMoveTolerance = function (newValue) { + if (utils.isNumber(newValue)) { + scope.pointerMoveTolerance = newValue; + + return this; + } + + return scope.pointerMoveTolerance; +}; + +/*\ + * interact.maxInteractions + [ method ] + ** + * 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. + ** + - newValue (number) #optional Any number. newValue <= 0 means no interactions. +\*/ +interact.maxInteractions = function (newValue) { + if (utils.isNumber(newValue)) { + scope.maxInteractions = newValue; + + return this; + } + + return scope.maxInteractions; +}; + +scope.interact = interact; + +module.exports = interact; diff --git a/src/legacyBrowsers.js b/src/legacyBrowsers.js new file mode 100644 index 000000000..f58bb7614 --- /dev/null +++ b/src/legacyBrowsers.js @@ -0,0 +1,60 @@ +const scope = require('./scope'); +const events = require('./utils/events'); +const signals = require('./utils/signals'); +const browser = require('./utils/browser'); +const iFinder = require('./utils/interactionFinder'); + +const toString = Object.prototype.toString; + +if (!window.Array.isArray) { + window.Array.isArray = function (obj) { + return toString.call(obj) === '[object Array]'; + }; +} + +if (!String.prototype.trim) { + String.prototype.trim = function () { + return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); + }; +} + +// http://www.quirksmode.org/dom/events/click.html +// >Events leading to dblclick +// +// IE8 doesn't fire down event before dblclick. +// This workaround tries to fire a tap and doubletap after dblclick +function onIE8Dblclick (event) { + const interaction = iFinder.search(event, event.type, event.target); + + if (!interaction) { return; } + + if (interaction.prevTap + && event.clientX === interaction.prevTap.clientX + && event.clientY === interaction.prevTap.clientY + && event.target === interaction.prevTap.target) { + + interaction.downTargets[0] = event.target; + interaction.downTimes [0] = new Date().getTime(); + + scope.pointerEvents.collectEventTargets(interaction, event, event, event.target, 'tap'); + } +} + +if (browser.isIE8) { + signals.on('listen-to-document', function ({ doc }) { + // For IE's lack of Event#preventDefault + events.add(doc, 'selectstart', function (event) { + const interaction = scope.interactions[0]; + + if (interaction.currentAction()) { + interaction.checkAndPreventDefault(event); + } + }); + + if (scope.pointerEvents) { + events.add(doc, 'dblclick', onIE8Dblclick); + } + }); +} + +module.exports = null; diff --git a/src/modifiers/base.js b/src/modifiers/base.js new file mode 100644 index 000000000..1c82a29cf --- /dev/null +++ b/src/modifiers/base.js @@ -0,0 +1,64 @@ +const extend = require('../utils/extend'); + +const modifiers = { + names: [], + + setOffsets: function (interaction, interactable, element, rect, offsets) { + for (let i = 0; i < modifiers.names.length; i++) { + const modifierName = modifiers.names[i]; + + offsets[modifierName] = + modifiers[modifiers.names[i]].setOffset(interaction, + interactable, element, rect, + interaction.startOffset); + } + }, + + setAll: function (interaction, coordsArg, statuses, preEnd, requireEndOnly) { + const result = { + dx: 0, + dy: 0, + changed: false, + locked: false, + shouldMove: true, + }; + const target = interaction.target; + const coords = extend({}, coordsArg); + + let currentStatus; + + for (const modifierName of modifiers.names) { + const modifier = modifiers[modifierName]; + + if (!modifier.shouldDo(target, interaction.prepared.name, preEnd, requireEndOnly)) { continue; } + + currentStatus = modifier.set(coords, interaction, statuses[modifierName]); + + if (currentStatus.locked) { + coords.x += currentStatus.dx; + coords.y += currentStatus.dy; + + result.dx += currentStatus.dx; + result.dy += currentStatus.dy; + + result.locked = true; + } + } + + // a move should be fired if the modified coords of + // the last modifier status that was calculated changes + result.shouldMove = !currentStatus || currentStatus.changed; + + return result; + }, + + resetStatuses: function (statuses) { + for (const modifierName of modifiers.names) { + statuses[modifierName] = modifiers[modifierName].reset(statuses[modifierName] || {}); + } + + return statuses; + }, +}; + +module.exports = modifiers; diff --git a/src/modifiers/restrict.js b/src/modifiers/restrict.js new file mode 100644 index 000000000..ce7772e80 --- /dev/null +++ b/src/modifiers/restrict.js @@ -0,0 +1,153 @@ +const modifiers = require('./base'); +const utils = require('../utils'); +const defaultOptions = require('../defaultOptions'); + +const restrict = { + defaults: { + enabled : false, + endOnly : false, + restriction: null, + elementRect: null, + }, + + shouldDo: function (interactable, actionName, preEnd, requireEndOnly) { + const restrictOptions = interactable.options[actionName].restrict; + + return (restrictOptions && restrictOptions.enabled + && (preEnd || !restrictOptions.endOnly) + && (!requireEndOnly || restrictOptions.endOnly)); + }, + + setOffset: function (interaction, interactable, element, rect, startOffset) { + const elementRect = interactable.options[interaction.prepared.name].restrict.elementRect; + const offset = {}; + + if (rect && elementRect) { + 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)); + } + else { + offset.left = offset.top = offset.right = offset.bottom = 0; + } + + return offset; + }, + + set: function (pageCoords, interaction, status) { + const target = interaction.target; + const restrictOptions = target && target.options[interaction.prepared.name].restrict; + let restriction = restrictOptions && restrictOptions.restriction; + + if (!restriction) { + return status; + } + + const page = status.useStatusXY + ? { x: status.x, y: status.y } + : utils.extend({}, pageCoords); + + page.x -= interaction.inertiaStatus.resumeDx; + page.y -= interaction.inertiaStatus.resumeDy; + + status.dx = 0; + status.dy = 0; + status.locked = false; + + let rect; + let restrictedX; + let restrictedY; + + if (utils.isString(restriction)) { + if (restriction === 'parent') { + restriction = utils.parentElement(interaction.element); + } + else if (restriction === 'self') { + restriction = target.getRect(interaction.element); + } + else { + restriction = utils.closest(interaction.element, restriction); + } + + if (!restriction) { return status; } + } + + if (utils.isFunction(restriction)) { + restriction = restriction(page.x, page.y, interaction.element); + } + + if (utils.isElement(restriction)) { + restriction = utils.getElementRect(restriction); + } + + rect = restriction; + + const offset = interaction.modifierOffsets.restrict; + + if (!restriction) { + restrictedX = page.x; + restrictedY = page.y; + } + // object is assumed to have + // x, y, width, height or + // left, top, right, bottom + else if ('x' in restriction && 'y' in restriction) { + restrictedX = Math.max(Math.min(rect.x + rect.width - offset.right , page.x), rect.x + offset.left); + restrictedY = Math.max(Math.min(rect.y + rect.height - offset.bottom, page.y), rect.y + offset.top ); + } + else { + restrictedX = Math.max(Math.min(rect.right - offset.right , page.x), rect.left + offset.left); + restrictedY = Math.max(Math.min(rect.bottom - offset.bottom, page.y), rect.top + offset.top ); + } + + status.dx = restrictedX - page.x; + status.dy = restrictedY - page.y; + + status.changed = status.restrictedX !== restrictedX || status.restrictedY !== restrictedY; + status.locked = !!(status.dx || status.dy); + + status.restrictedX = restrictedX; + status.restrictedY = restrictedY; + + return status; + }, + + reset: function (status) { + status.dx = status.dy = 0; + status.modifiedX = status.modifiedY = NaN; + status.locked = false; + status.changed = true; + + return status; + }, + + modifyCoords: function (page, client, interactable, status, actionName, phase) { + const options = interactable.options[actionName].restrict; + const elementRect = options && options.elementRect; + + if (modifiers.restrict.shouldDo(interactable, actionName) + && !(phase === 'start' && elementRect && status.locked)) { + + if (status.locked) { + page.x += status.dx; + page.y += status.dy; + client.x += status.dx; + client.y += status.dy; + + return { + dx: status.dx, + dy: status.dy, + }; + } + } + }, +}; + +modifiers.restrict = restrict; +modifiers.names.push('restrict'); + +defaultOptions.perAction.restrict = restrict.defaults; + +module.exports = restrict; diff --git a/src/modifiers/snap.js b/src/modifiers/snap.js new file mode 100644 index 000000000..67237a29f --- /dev/null +++ b/src/modifiers/snap.js @@ -0,0 +1,239 @@ +const modifiers = require('./base'); +const interact = require('../interact'); +const utils = require('../utils'); +const defaultOptions = require('../defaultOptions'); + +const snap = { + defaults: { + enabled: false, + endOnly: false, + range : Infinity, + targets: null, + offsets: null, + + relativePoints: null, + }, + + shouldDo: function (interactable, actionName, preEnd, requireEndOnly) { + const snapOptions = interactable.options[actionName].snap; + + return (snapOptions && snapOptions.enabled + && (preEnd || !snapOptions.endOnly) + && (!requireEndOnly || snapOptions.endOnly)); + }, + + setOffset: function (interaction, interactable, element, rect, startOffset) { + const offsets = []; + const origin = utils.getOriginXY(interactable, element); + const snapOptions = interactable.options[interaction.prepared.name].snap; + const snapOffset = (snapOptions && snapOptions.offset === 'startCoords' + ? { + x: interaction.startCoords.page.x - origin.x, + y: interaction.startCoords.page.y - origin.y, + } + : snapOptions && snapOptions.offset || { x: 0, y: 0 }); + + if (rect && snapOptions && snapOptions.relativePoints && snapOptions.relativePoints.length) { + for (const { x: relativeX, y: relativeY } of snapOptions.relativePoints) { + offsets.push({ + x: startOffset.left - (rect.width * relativeX) + snapOffset.x, + y: startOffset.top - (rect.height * relativeY) + snapOffset.y, + }); + } + } + else { + offsets.push(snapOffset); + } + + return offsets; + }, + + set: function (pageCoords, interaction, status) { + const snapOptions = interaction.target.options[interaction.prepared.name].snap; + const targets = []; + let target; + let page; + let i; + + if (status.useStatusXY) { + page = { x: status.x, y: status.y }; + } + else { + const origin = utils.getOriginXY(interaction.target, interaction.element); + + page = utils.extend({}, pageCoords); + + page.x -= origin.x; + page.y -= origin.y; + } + + status.realX = page.x; + status.realY = page.y; + + page.x -= interaction.inertiaStatus.resumeDx; + page.y -= interaction.inertiaStatus.resumeDy; + + const offsets = interaction.modifierOffsets.snap; + let len = snapOptions.targets? snapOptions.targets.length : 0; + + for (const { x: offsetX, y: offsetY } of offsets) { + const relativeX = page.x - offsetX; + const relativeY = page.y - offsetY; + + for (const snapTarget of snapOptions.targets) { + if (utils.isFunction(snapTarget)) { + target = snapTarget(relativeX, relativeY, interaction); + } + else { + target = snapTarget; + } + + if (!target) { continue; } + + targets.push({ + x: utils.isNumber(target.x) ? (target.x + offsetX) : relativeX, + y: utils.isNumber(target.y) ? (target.y + offsetY) : relativeY, + + range: utils.isNumber(target.range)? target.range: snapOptions.range, + }); + } + } + + const closest = { + target: null, + inRange: false, + distance: 0, + range: 0, + dx: 0, + dy: 0, + }; + + for (i = 0, len = targets.length; i < len; i++) { + target = targets[i]; + + const range = target.range; + const dx = target.x - page.x; + const dy = target.y - page.y; + const distance = utils.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.dx = dx; + closest.dy = dy; + + status.range = range; + } + } + + let snapChanged; + + if (closest.target) { + snapChanged = (status.snappedX !== closest.target.x || status.snappedY !== closest.target.y); + + status.snappedX = closest.target.x; + status.snappedY = closest.target.y; + } + else { + snapChanged = true; + + status.snappedX = NaN; + status.snappedY = NaN; + } + + status.dx = closest.dx; + status.dy = closest.dy; + + status.changed = (snapChanged || (closest.inRange && !status.locked)); + status.locked = closest.inRange; + + return status; + }, + + reset: function (status) { + status.dx = status.dy = 0; + status.snappedX = status.snappedY = NaN; + status.locked = false; + status.changed = true; + + return status; + }, + + modifyCoords: function (page, client, interactable, status, actionName, phase) { + const snapOptions = interactable.options[actionName].snap; + const relativePoints = snapOptions && snapOptions.relativePoints; + + if (snapOptions && snapOptions.enabled + && !(phase === 'start' && relativePoints && relativePoints.length)) { + + if (status.locked) { + page.x += status.dx; + page.y += status.dy; + client.x += status.dx; + client.y += status.dy; + } + + return { + range : status.range, + locked : status.locked, + x : status.snappedX, + y : status.snappedY, + realX : status.realX, + realY : status.realY, + dx : status.dx, + dy : status.dy, + }; + } + }, +}; + +interact.createSnapGrid = function (grid) { + return function (x, y) { + let offsetX = 0; + let offsetY = 0; + + if (utils.isObject(grid.offset)) { + offsetX = grid.offset.x; + offsetY = grid.offset.y; + } + + const gridx = Math.round((x - offsetX) / grid.x); + const gridy = Math.round((y - offsetY) / grid.y); + + const newX = gridx * grid.x + offsetX; + const newY = gridy * grid.y + offsetY; + + return { + x: newX, + y: newY, + range: grid.range, + }; + }; +}; + +modifiers.snap = snap; +modifiers.names.push('snap'); + +defaultOptions.perAction.snap = snap.defaults; + +module.exports = snap; diff --git a/src/pointerEvents.js b/src/pointerEvents.js new file mode 100644 index 000000000..89decdb9c --- /dev/null +++ b/src/pointerEvents.js @@ -0,0 +1,213 @@ +const scope = require('./scope'); +const InteractEvent = require('./InteractEvent'); +const utils = require('./utils'); +const browser = require('./utils/browser'); +const signals = require('./utils/signals'); + +const simpleSignals = [ + 'interaction-down', + 'interaction-up', + 'interaction-up', + 'interaction-cancel', +]; +const simpleEvents = [ + 'down', + 'up', + 'tap', + 'cancel', +]; + +function preventOriginalDefault () { + this.originalEvent.preventDefault(); +} + +function firePointers (interaction, pointer, event, eventTarget, targets, elements, eventType) { + const pointerIndex = interaction.mouse? 0 : utils.indexOf(interaction.pointerIds, utils.getPointerId(pointer)); + let pointerEvent = {}; + let i; + // for tap events + let interval; + let createNewDoubleTap; + + // if it's a doubletap then the event properties would have been + // copied from the tap event and provided as the pointer argument + if (eventType === 'doubletap') { + pointerEvent = pointer; + } + else { + utils.pointerExtend(pointerEvent, event); + if (event !== pointer) { + utils.pointerExtend(pointerEvent, pointer); + } + + pointerEvent.preventDefault = preventOriginalDefault; + pointerEvent.stopPropagation = InteractEvent.prototype.stopPropagation; + pointerEvent.stopImmediatePropagation = InteractEvent.prototype.stopImmediatePropagation; + pointerEvent.interaction = interaction; + + pointerEvent.timeStamp = new Date().getTime(); + pointerEvent.originalEvent = event; + pointerEvent.type = eventType; + pointerEvent.pointerId = utils.getPointerId(pointer); + pointerEvent.pointerType = interaction.mouse? 'mouse' : !browser.supportsPointerEvent? 'touch' + : utils.isString(pointer.pointerType) + ? pointer.pointerType + : [undefined, undefined,'touch', 'pen', 'mouse'][pointer.pointerType]; + } + + if (eventType === 'tap') { + pointerEvent.dt = pointerEvent.timeStamp - interaction.downTimes[pointerIndex]; + + interval = pointerEvent.timeStamp - interaction.tapTime; + createNewDoubleTap = !!(interaction.prevTap && interaction.prevTap.type !== 'doubletap' + && interaction.prevTap.target === pointerEvent.target + && interval < 500); + + pointerEvent.double = createNewDoubleTap; + + interaction.tapTime = pointerEvent.timeStamp; + } + + for (i = 0; i < targets.length; i++) { + pointerEvent.currentTarget = elements[i]; + pointerEvent.interactable = targets[i]; + targets[i].fire(pointerEvent); + + if (pointerEvent.immediatePropagationStopped + || (pointerEvent.propagationStopped + && elements[i + 1] !== pointerEvent.currentTarget)) { + break; + } + } + + if (createNewDoubleTap) { + const doubleTap = {}; + + utils.extend(doubleTap, pointerEvent); + + doubleTap.dt = interval; + doubleTap.type = 'doubletap'; + + collectEventTargets(interaction, doubleTap, event, eventTarget, 'doubletap'); + + interaction.prevTap = doubleTap; + } + else if (eventType === 'tap') { + interaction.prevTap = pointerEvent; + } +} + +function collectEventTargets (interaction, pointer, event, eventTarget, eventType) { + const pointerIndex = interaction.mouse? 0 : utils.indexOf(interaction.pointerIds, utils.getPointerId(pointer)); + + // do not fire a tap event if the pointer was moved before being lifted + if (eventType === 'tap' && (interaction.pointerWasMoved + // or if the pointerup target is different to the pointerdown target + || !(interaction.downTargets[pointerIndex] && interaction.downTargets[pointerIndex] === eventTarget))) { + return; + } + + const targets = []; + const elements = []; + let element = eventTarget; + + function collectSelectors (interactable, selector, context) { + const els = browser.useMatchesSelectorPolyfill + ? context.querySelectorAll(selector) + : undefined; + + if (interactable._iEvents[eventType] + && utils.isElement(element) + && scope.inContext(interactable, element) + && !scope.testIgnore(interactable, element, eventTarget) + && scope.testAllow(interactable, element, eventTarget) + && utils.matchesSelector(element, selector, els)) { + + targets.push(interactable); + elements.push(element); + } + } + + const interact = scope.interact; + + while (element) { + if (interact.isSet(element) && interact(element)._iEvents[eventType]) { + targets.push(interact(element)); + elements.push(element); + } + + scope.interactables.forEachSelector(collectSelectors); + + element = utils.parentElement(element); + } + + // create the tap event even if there are no listeners so that + // doubletap can still be created and fired + if (targets.length || eventType === 'tap') { + firePointers(interaction, pointer, event, eventTarget, targets, elements, eventType); + } +} + +signals.on('interaction-move', function ({ interaction, pointer, event, eventTarget, duplicateMove }) { + const pointerIndex = (interaction.mouse + ? 0 + : utils.indexOf(interaction.pointerIds, utils.getPointerId(pointer))); + + if (!duplicateMove && (!interaction.pointerIsDown || interaction.pointerWasMoved)) { + if (interaction.pointerIsDown) { + clearTimeout(interaction.holdTimers[pointerIndex]); + } + + collectEventTargets(interaction, pointer, event, eventTarget, 'move'); + } +}); + +signals.on('interaction-down', function ({ interaction, pointer, event, eventTarget, pointerIndex }) { + // copy event to be used in timeout for IE8 + const eventCopy = browser.isIE8? utils.extend({}, event) : event; + + interaction.holdTimers[pointerIndex] = setTimeout(function () { + + collectEventTargets(interaction, + browser.isIE8? eventCopy : pointer, + eventCopy, + eventTarget, + 'hold'); + + }, scope.defaultOptions._holdDuration); +}); + +function createSignalListener (event) { + return function (arg) { + collectEventTargets(arg.interaction, + arg.pointer, + arg.event, + arg.eventTarget, + event); + }; +} + +for (let i = 0; i < simpleSignals.length; i++) { + signals.on(simpleSignals[i], createSignalListener(simpleEvents[i])); +} + +signals.on('interaction-new', function (interaction) { + interaction.prevTap = null; // the most recent tap event on this interaction + interaction.tapTime = 0; // time of the most recent tap event +}); + +utils.merge(scope.eventTypes, [ + 'down', + 'move', + 'up', + 'cancel', + 'tap', + 'doubletap', + 'hold', +]); + +module.exports = scope.pointerEvents = { + firePointers, + collectEventTargets, + preventOriginalDefault, +}; diff --git a/src/scope.js b/src/scope.js new file mode 100644 index 000000000..05f8f72a6 --- /dev/null +++ b/src/scope.js @@ -0,0 +1,71 @@ +const scope = {}; +const utils = require('./utils'); +const signals = require('./utils/signals'); + +scope.defaultOptions = require('./defaultOptions'); +scope.events = require('./utils/events'); +scope.signals = require('./utils/signals'); + +utils.extend(scope, require('./utils/window')); +utils.extend(scope, require('./utils/domObjects')); + +scope.documents = []; // all documents being listened to +scope.eventTypes = []; // all event types specific to interact.js + +scope.withinInteractionLimit = function (interactable, element, action) { + const options = interactable.options; + const maxActions = options[action.name].max; + const maxPerElement = options[action.name].maxPerElement; + let activeInteractions = 0; + let targetCount = 0; + let targetElementCount = 0; + + for (let i = 0, len = scope.interactions.length; i < len; i++) { + const interaction = scope.interactions[i]; + const otherAction = interaction.prepared.name; + + if (!interaction.interacting()) { continue; } + + activeInteractions++; + + if (activeInteractions >= scope.maxInteractions) { + return false; + } + + if (interaction.target !== interactable) { continue; } + + targetCount += (otherAction === action.name)|0; + + if (targetCount >= maxActions) { + return false; + } + + if (interaction.element === element) { + targetElementCount++; + + if (otherAction !== action.name || targetElementCount >= maxPerElement) { + return false; + } + } + } + + return scope.maxInteractions > 0; +}; + +scope.endAllInteractions = function (event) { + for (let i = 0; i < scope.interactions.length; i++) { + scope.interactions[i].pointerEnd(event, event); + } +}; + +scope.prefixedPropREs = utils.prefixedPropREs; + +signals.on('listen-to-document', function ({ doc }) { + // if document is already known + if (utils.contains(scope.documents, doc)) { + // don't call any further signal listeners + return false; + } +}); + +module.exports = scope; diff --git a/src/utils/arr.js b/src/utils/arr.js new file mode 100644 index 000000000..4df28f15a --- /dev/null +++ b/src/utils/arr.js @@ -0,0 +1,27 @@ +function indexOf (array, target) { + for (let i = 0, len = array.length; i < len; i++) { + if (array[i] === target) { + return i; + } + } + + return -1; +} + +function contains (array, target) { + return indexOf(array, target) !== -1; +} + +function merge (target, source) { + for (let i = 0; i < source.length; i++) { + target.push(source[i]); + } + + return target; +} + +module.exports = { + indexOf, + contains, + merge, +}; diff --git a/src/utils/browser.js b/src/utils/browser.js new file mode 100644 index 000000000..01dbeaf16 --- /dev/null +++ b/src/utils/browser.js @@ -0,0 +1,46 @@ +const win = require('./window'); +const isType = require('./isType'); +const domObjects = require('./domObjects'); + +const browser = { + // Does the browser support touch input? + supportsTouch: !!(('ontouchstart' in win.window) || win.window.DocumentTouch + && domObjects.document instanceof win.DocumentTouch), + + // Does the browser support PointerEvents + supportsPointerEvent: !!domObjects.PointerEvent, + + isIE8: ('attachEvent' in win.window) && !('addEventListener' in win.window), + + // Opera Mobile must be handled differently + isOperaMobile: (navigator.appName === 'Opera' + && browser.supportsTouch + && navigator.userAgent.match('Presto')), + + // scrolling doesn't change the result of getClientRects on iOS 7 + isIOS7: (/iP(hone|od|ad)/.test(navigator.platform) + && /OS 7[^\d]/.test(navigator.appVersion)), + + isIe9OrOlder: domObjects.document.all && !win.window.atob, + + // prefix matchesSelector + prefixedMatchesSelector: 'matches' in Element.prototype + ? 'matches': 'webkitMatchesSelector' in Element.prototype + ? 'webkitMatchesSelector': 'mozMatchesSelector' in Element.prototype + ? 'mozMatchesSelector': 'oMatchesSelector' in Element.prototype + ? 'oMatchesSelector': 'msMatchesSelector', + + useMatchesSelectorPolyfill: false, + + pEventTypes: (domObjects.PointerEvent + ? (domObjects.PointerEvent === win.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), +}; + +browser.useMatchesSelectorPolyfill = !isType.isFunction(Element.prototype[browser.prefixedMatchesSelector]); + +module.exports = browser; diff --git a/src/utils/domObjects.js b/src/utils/domObjects.js new file mode 100644 index 000000000..f0e86eecb --- /dev/null +++ b/src/utils/domObjects.js @@ -0,0 +1,17 @@ +const domObjects = {}; +const win = require('./window').window; + +function blank () {} + +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.HTMLElement = win.HTMLElement || win.Element; + +domObjects.Event = win.Event; +domObjects.Touch = win.Touch || blank; +domObjects.PointerEvent = (win.PointerEvent || win.MSPointerEvent); + +module.exports = domObjects; diff --git a/src/utils/domUtils.js b/src/utils/domUtils.js new file mode 100644 index 000000000..8f3b750cc --- /dev/null +++ b/src/utils/domUtils.js @@ -0,0 +1,238 @@ +const win = require('./window'); +const browser = require('./browser'); +const isType = require('./isType'); +const domObjects = require('./domObjects'); + +const domUtils = { + nodeContains: function (parent, child) { + while (child) { + if (child === parent) { + return true; + } + + child = child.parentNode; + } + + return false; + }, + + closest: function (child, selector) { + let parent = domUtils.parentElement(child); + + while (isType.isElement(parent)) { + if (domUtils.matchesSelector(parent, selector)) { return parent; } + + parent = domUtils.parentElement(parent); + } + + return null; + }, + + parentElement: function (node) { + let parent = node.parentNode; + + if (isType.isDocFrag(parent)) { + // skip past #shado-root fragments + while ((parent = parent.host) && isType.isDocFrag(parent)) { + continue; + } + + return parent; + } + + return parent; + }, + + // taken from http://tanalin.com/en/blog/2012/12/matches-selector-ie8/ and modified + matchesSelectorPolyfill: browser.useMatchesSelectorPolyfill + ? function (element, selector, elems) { + elems = elems || element.parentNode.querySelectorAll(selector); + + for (let i = 0, len = elems.length; i < len; i++) { + if (elems[i] === element) { + return true; + } + } + + return false; + } + : null, + + matchesSelector: function (element, selector, nodeList) { + if (browser.useMatchesSelectorPolyfill) { + return domUtils.matchesSelectorPolyfill(element, selector, nodeList); + } + + // remove /deep/ from selectors if shadowDOM polyfill is used + if (win.window !== win.realWindow) { + selector = selector.replace(/\/deep\//g, ' '); + } + + return element[browser.prefixedMatchesSelector](selector); + }, + + // Test for the element that's "above" all other qualifiers + indexOfDeepestElement: function (elements) { + let deepestZoneParents = []; + let dropzoneParents = []; + let dropzone; + let deepestZone = elements[0]; + let index = deepestZone? 0: -1; + let parent; + let child; + let i; + let n; + + for (i = 1; i < elements.length; i++) { + dropzone = elements[i]; + + // an element might belong to multiple selector dropzones + if (!dropzone || dropzone === deepestZone) { + continue; + } + + if (!deepestZone) { + deepestZone = dropzone; + index = i; + continue; + } + + // check if the deepest or current are document.documentElement or document.rootElement + // - if the current dropzone is, do nothing and continue + if (dropzone.parentNode === dropzone.ownerDocument) { + continue; + } + // - if deepest is, update with the current dropzone and continue to next + else if (deepestZone.parentNode === dropzone.ownerDocument) { + deepestZone = dropzone; + index = i; + continue; + } + + if (!deepestZoneParents.length) { + parent = deepestZone; + while (parent.parentNode && parent.parentNode !== parent.ownerDocument) { + deepestZoneParents.unshift(parent); + parent = parent.parentNode; + } + } + + // if this element is an svg element and the current deepest is + // an HTMLElement + if (deepestZone instanceof domObjects.HTMLElement + && dropzone instanceof domObjects.SVGElement + && !(dropzone instanceof domObjects.SVGSVGElement)) { + + if (dropzone === deepestZone.parentNode) { + continue; + } + + parent = dropzone.ownerSVGElement; + } + else { + parent = dropzone; + } + + dropzoneParents = []; + + while (parent.parentNode !== parent.ownerDocument) { + dropzoneParents.unshift(parent); + parent = parent.parentNode; + } + + n = 0; + + // get (position of last common ancestor) + 1 + while (dropzoneParents[n] && dropzoneParents[n] === deepestZoneParents[n]) { + n++; + } + + const parents = [ + dropzoneParents[n - 1], + dropzoneParents[n], + deepestZoneParents[n], + ]; + + child = parents[0].lastChild; + + while (child) { + if (child === parents[1]) { + deepestZone = dropzone; + index = i; + deepestZoneParents = []; + + break; + } + else if (child === parents[2]) { + break; + } + + child = child.previousSibling; + } + } + + return index; + }, + + matchesUpTo: function (element, selector, limit) { + while (domUtils.isElement(element)) { + if (domUtils.matchesSelector(element, selector)) { + return true; + } + + element = domUtils.parentElement(element); + + if (element === limit) { + return domUtils.matchesSelector(element, selector); + } + } + + return false; + }, + + getActualElement: function (element) { + return (element instanceof domObjects.SVGElementInstance + ? element.correspondingUseElement + : element); + }, + + getScrollXY: function (relevantWindow) { + relevantWindow = relevantWindow || win.window; + return { + x: relevantWindow.scrollX || relevantWindow.document.documentElement.scrollLeft, + y: relevantWindow.scrollY || relevantWindow.document.documentElement.scrollTop, + }; + }, + + getElementClientRect: function (element) { + 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, + }; + }, + + getElementRect: function (element) { + const clientRect = domUtils.getElementClientRect(element); + + if (!browser.isIOS7 && clientRect) { + const scroll = domUtils.getScrollXY(win.getWindow(element)); + + clientRect.left += scroll.x; + clientRect.right += scroll.x; + clientRect.top += scroll.y; + clientRect.bottom += scroll.y; + } + + return clientRect; + }, +}; + +module.exports = domUtils; diff --git a/src/utils/events.js b/src/utils/events.js new file mode 100644 index 000000000..2efa32e0e --- /dev/null +++ b/src/utils/events.js @@ -0,0 +1,341 @@ +const arr = require('./arr'); +const isType = require('./isType'); +const domUtils = require('./domUtils'); +const indexOf = arr.indexOf; +const contains = arr.contains; +const getWindow = require('./window').getWindow; + +const useAttachEvent = ('attachEvent' in window) && !('addEventListener' in window); +const addEvent = useAttachEvent? 'attachEvent': 'addEventListener'; +const removeEvent = useAttachEvent? 'detachEvent': 'removeEventListener'; +const on = useAttachEvent? 'on': ''; + +const elements = []; +const targets = []; +const attachedListeners = []; + +// { +// type: { +// selectors: ['selector', ...], +// contexts : [document, ...], +// listeners: [[listener, useCapture], ...] +// } +// } +const delegatedEvents = {}; + +const documents = []; + +function add (element, type, listener, useCapture) { + let elementIndex = indexOf(elements, element); + let target = targets[elementIndex]; + + if (!target) { + target = { + events: {}, + typeCount: 0, + }; + + elementIndex = elements.push(element) - 1; + targets.push(target); + + attachedListeners.push((useAttachEvent ? { + supplied: [], + wrapped : [], + useCount: [], + } : null)); + } + + if (!target.events[type]) { + target.events[type] = []; + target.typeCount++; + } + + if (!contains(target.events[type], listener)) { + let ret; + + if (useAttachEvent) { + const { supplied, wrapped, useCount } = attachedListeners[elementIndex]; + const listenerIndex = indexOf(supplied, listener); + + const wrappedListener = wrapped[listenerIndex] || function (event) { + if (!event.immediatePropagationStopped) { + event.target = event.srcElement; + event.currentTarget = element; + + event.preventDefault = event.preventDefault || preventDef; + event.stopPropagation = event.stopPropagation || stopProp; + event.stopImmediatePropagation = event.stopImmediatePropagation || stopImmProp; + + if (/mouse|click/.test(event.type)) { + event.pageX = event.clientX + getWindow(element).document.documentElement.scrollLeft; + event.pageY = event.clientY + getWindow(element).document.documentElement.scrollTop; + } + + listener(event); + } + }; + + ret = element[addEvent](on + type, wrappedListener, !!useCapture); + + if (listenerIndex === -1) { + supplied.push(listener); + wrapped.push(wrappedListener); + useCount.push(1); + } + else { + useCount[listenerIndex]++; + } + } + else { + ret = element[addEvent](type, listener, !!useCapture); + } + target.events[type].push(listener); + + return ret; + } +} + +function remove (element, type, listener, useCapture) { + const elementIndex = indexOf(elements, element); + const target = targets[elementIndex]; + + if (!target || !target.events) { + return; + } + + let wrappedListener = listener; + let listeners; + let listenerIndex; + + if (useAttachEvent) { + listeners = attachedListeners[elementIndex]; + listenerIndex = indexOf(listeners.supplied, listener); + wrappedListener = listeners.wrapped[listenerIndex]; + } + + if (type === 'all') { + for (type in target.events) { + if (target.events.hasOwnProperty(type)) { + remove(element, type, 'all'); + } + } + return; + } + + if (target.events[type]) { + const len = target.events[type].length; + + if (listener === 'all') { + for (let i = 0; i < len; i++) { + remove(element, type, target.events[type][i], !!useCapture); + } + return; + } + else { + for (let i = 0; i < len; i++) { + if (target.events[type][i] === listener) { + element[removeEvent](on + type, wrappedListener, !!useCapture); + target.events[type].splice(i, 1); + + if (useAttachEvent && listeners) { + listeners.useCount[listenerIndex]--; + if (listeners.useCount[listenerIndex] === 0) { + listeners.supplied.splice(listenerIndex, 1); + listeners.wrapped.splice(listenerIndex, 1); + listeners.useCount.splice(listenerIndex, 1); + } + } + + break; + } + } + } + + if (target.events[type] && target.events[type].length === 0) { + target.events[type] = null; + target.typeCount--; + } + } + + if (!target.typeCount) { + targets.splice(elementIndex, 1); + elements.splice(elementIndex, 1); + attachedListeners.splice(elementIndex, 1); + } +} + +function addDelegate (selector, context, type, listener, useCapture) { + if (!delegatedEvents[type]) { + delegatedEvents[type] = { + selectors: [], + contexts : [], + listeners: [], + }; + + // add delegate listener functions + for (let i = 0; i < documents.length; i++) { + add(documents[i], type, delegateListener); + add(documents[i], type, delegateUseCapture, true); + } + } + + const delegated = delegatedEvents[type]; + let index; + + for (index = delegated.selectors.length - 1; index >= 0; index--) { + if (delegated.selectors[index] === selector + && delegated.contexts[index] === context) { + break; + } + } + + if (index === -1) { + index = delegated.selectors.length; + + delegated.selectors.push(selector); + delegated.contexts .push(context); + delegated.listeners.push([]); + } + + // keep listener and useCapture flag + delegated.listeners[index].push([listener, useCapture]); +} + +function removeDelegate (selector, context, type, listener, useCapture) { + const delegated = delegatedEvents[type]; + let matchFound = false; + let index; + + if (!delegated) { return; } + + // count from last index of delegated to 0 + for (index = delegated.selectors.length - 1; index >= 0; index--) { + // look for matching selector and context Node + if (delegated.selectors[index] === selector + && delegated.contexts[index] === context) { + + const listeners = delegated.listeners[index]; + + // each item of the listeners array is an array: [function, useCaptureFlag] + for (let i = listeners.length - 1; i >= 0; i--) { + const fn = listeners[i][0]; + const useCap = listeners[i][1]; + + // check if the listener functions and useCapture flags match + if (fn === listener && useCap === useCapture) { + // remove the listener from the array of listeners + listeners.splice(i, 1); + + // if all listeners for this interactable have been removed + // remove the interactable from the delegated arrays + if (!listeners.length) { + delegated.selectors.splice(index, 1); + delegated.contexts .splice(index, 1); + delegated.listeners.splice(index, 1); + + // remove delegate function from context + remove(context, type, delegateListener); + remove(context, type, delegateUseCapture, true); + + // remove the arrays if they are empty + if (!delegated.selectors.length) { + delegatedEvents[type] = null; + } + } + + // 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, useCapture) { + const fakeEvent = {}; + const delegated = delegatedEvents[event.type]; + const eventTarget = (domUtils.getActualElement(event.path + ? event.path[0] + : event.target)); + let element = eventTarget; + + useCapture = useCapture? true: false; + + // duplicate the event so that currentTarget can be changed + for (const prop in event) { + fakeEvent[prop] = event[prop]; + } + + fakeEvent.originalEvent = event; + fakeEvent.preventDefault = preventOriginalDefault; + + // climb up document tree looking for selector matches + while (isType.isElement(element)) { + for (let i = 0; i < delegated.selectors.length; i++) { + const selector = delegated.selectors[i]; + const context = delegated.contexts[i]; + + if (domUtils.matchesSelector(element, selector) + && domUtils.nodeContains(context, eventTarget) + && domUtils.nodeContains(context, element)) { + + const listeners = delegated.listeners[i]; + + fakeEvent.currentTarget = element; + + for (let j = 0; j < listeners.length; j++) { + if (listeners[j][1] === useCapture) { + listeners[j][0](fakeEvent); + } + } + } + } + + element = domUtils.parentElement(element); + } +} + +function delegateUseCapture (event) { + return delegateListener.call(this, event, true); +} + +function preventDef () { + this.returnValue = false; +} + +function preventOriginalDefault () { + this.originalEvent.preventDefault(); +} + +function stopProp () { + this.cancelBubble = true; +} + +function stopImmProp () { + this.cancelBubble = true; + this.immediatePropagationStopped = true; +} + +module.exports = { + add, + remove, + + addDelegate, + removeDelegate, + + delegateListener, + delegateUseCapture, + delegatedEvents, + documents, + + useAttachEvent, + + _elements: elements, + _targets: targets, + _attachedListeners: attachedListeners, +}; diff --git a/src/utils/extend.js b/src/utils/extend.js new file mode 100644 index 000000000..91402bf1c --- /dev/null +++ b/src/utils/extend.js @@ -0,0 +1,6 @@ +module.exports = function extend (dest, source) { + for (const prop in source) { + dest[prop] = source[prop]; + } + return dest; +}; diff --git a/src/utils/getOriginXY.js b/src/utils/getOriginXY.js new file mode 100644 index 000000000..5d7b04b03 --- /dev/null +++ b/src/utils/getOriginXY.js @@ -0,0 +1,29 @@ +const { closest, parentElement, getElementRect } = require('./domUtils'); +const { isElement, isFunction, trySelector } = require('./isType'); + +module.exports = function (interactable, element) { + let origin = interactable.options.origin; + + if (origin === 'parent') { + origin = parentElement(element); + } + else if (origin === 'self') { + origin = interactable.getRect(element); + } + else if (trySelector(origin)) { + origin = closest(element, origin) || { x: 0, y: 0 }; + } + + if (isFunction(origin)) { + origin = origin(interactable && element); + } + + if (isElement(origin)) { + origin = getElementRect(origin); + } + + origin.x = ('x' in origin)? origin.x : origin.left; + origin.y = ('y' in origin)? origin.y : origin.top; + + return origin; +}; diff --git a/src/utils/hypot.js b/src/utils/hypot.js new file mode 100644 index 000000000..36c7c0561 --- /dev/null +++ b/src/utils/hypot.js @@ -0,0 +1 @@ +module.exports = (x, y) => Math.sqrt(x * x + y * y); diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 000000000..06449154e --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,48 @@ +const utils = module.exports; +const extend = require('./extend'); +const win = require('./window'); + +utils.blank = function () {}; + +utils.warnOnce = function (method, message) { + let warned = false; + + return function () { + if (!warned) { + win.window.console.warn(message); + warned = true; + } + + return method.apply(this, arguments); + }; +}; + +// http://stackoverflow.com/a/5634528/2280888 +utils._getQBezierValue = function (t, p1, p2, p3) { + const iT = 1 - t; + return iT * iT * p1 + 2 * iT * t * p2 + t * t * p3; +}; + +utils.getQuadraticCurvePoint = function (startX, startY, cpX, cpY, endX, endY, position) { + return { + x: utils._getQBezierValue(position, startX, cpX, endX), + y: utils._getQBezierValue(position, startY, cpY, endY), + }; +}; + +// http://gizma.com/easing/ +utils.easeOutQuad = function (t, b, c, d) { + t /= d; + return -c * t*(t-2) + b; +}; + +utils.extend = extend; +utils.hypot = require('./hypot'); +utils.raf = require('./raf'); +utils.browser = require('./browser'); +utils.getOriginXY = require('./getOriginXY'); + +extend(utils, require('./arr')); +extend(utils, require('./isType')); +extend(utils, require('./domUtils')); +extend(utils, require('./pointerUtils')); diff --git a/src/utils/interactionFinder.js b/src/utils/interactionFinder.js new file mode 100644 index 000000000..553596775 --- /dev/null +++ b/src/utils/interactionFinder.js @@ -0,0 +1,97 @@ +const scope = require('../scope'); +const utils = require('./index'); +const browser = require('./browser'); + +const finder = { + methodOrder: [ 'inertiaResume', 'mouse', 'hasPointer', 'idle' ], + + search: function (pointer, eventType, eventTarget) { + const mouseEvent = (/mouse/i.test(pointer.pointerType || eventType) + // MSPointerEvent.MSPOINTER_TYPE_MOUSE + || pointer.pointerType === 4); + const pointerId = utils.getPointerId(pointer); + const details = { pointer, pointerId, mouseEvent, eventType, eventTarget }; + + for (const method of finder.methodOrder) { + const interaction = finder[method](details); + + if (interaction) { + return interaction; + } + } + }, + + // try to resume inertia with a new pointer + inertiaResume: function ({ mouseEvent, eventType, eventTarget }) { + if (!/down|start/i.test(eventType)) { + return null; + } + + for (const interaction of scope.interactions) { + let element = eventTarget; + + if (interaction.inertiaStatus.active && interaction.target.options[interaction.prepared.name].inertia.allowResume + && (interaction.mouse === mouseEvent)) { + while (element) { + // if the element is the interaction element + if (element === interaction.element) { + return interaction; + } + element = utils.parentElement(element); + } + } + } + + return null; + }, + + // if it's a mouse interaction + mouse: function ({ mouseEvent, eventType }) { + if (!mouseEvent && (browser.supportsTouch || browser.supportsPointerEvent)) { + return null; + } + + // Find a mouse interaction that's not in inertia phase + for (const interaction of scope.interactions) { + if (interaction.mouse && !interaction.inertiaStatus.active) { + return interaction; + } + } + + // Find any interaction specifically for mouse. + // If the eventType is a mousedown, and inertia is active + // ignore the interaction + for (const interaction of scope.interactions) { + if (interaction.mouse && !(/down/.test(eventType) && interaction.inertiaStatus.active)) { + return interaction; + } + } + + return null; + }, + + // get interaction that has this pointer + hasPointer: function ({ pointerId }) { + for (const interaction of scope.interactions) { + if (utils.contains(interaction.pointerIds, pointerId)) { + return interaction; + } + } + }, + + // get first idle interaction + idle: function ({ mouseEvent }) { + for (const interaction of scope.interactions) { + if ((!interaction.prepared.name || (interaction.target.options.gesture.enabled)) + && !interaction.interacting() + && !(!mouseEvent && interaction.mouse)) { + + return interaction; + } + } + + return null; + }, +}; + +module.exports = finder; diff --git a/src/utils/isType.js b/src/utils/isType.js new file mode 100644 index 000000000..5d09d8465 --- /dev/null +++ b/src/utils/isType.js @@ -0,0 +1,47 @@ +const win = require('./window'); +const isWindow = require('./isWindow'); +const domObjects = require('./domObjects'); + +const isType = { + isElement : function (o) { + if (!o || (typeof o !== 'object')) { return false; } + + const _window = win.getWindow(o) || win.window; + + return (/object|function/.test(typeof _window.Element) + ? o instanceof _window.Element //DOM2 + : o.nodeType === 1 && typeof o.nodeName === 'string'); + }, + + isArray : null, + + isWindow : function (thing) { return thing === win.window || isWindow(thing); }, + + isDocFrag : function (thing) { return !!thing && thing instanceof domObjects.DocumentFragment; }, + + isObject : function (thing) { return !!thing && (typeof thing === 'object'); }, + + isFunction : function (thing) { return typeof thing === 'function'; }, + + isNumber : function (thing) { return typeof thing === 'number' ; }, + + isBool : function (thing) { return typeof thing === 'boolean' ; }, + + isString : function (thing) { return typeof thing === 'string' ; }, + + trySelector: function (value) { + if (!isType.isString(value)) { return false; } + + // an exception will be raised if it is invalid + domObjects.document.querySelector(value); + return true; + }, +}; + +isType.isArray = function (thing) { + return (isType.isObject(thing) + && (typeof thing.length !== 'undefined') + && isType.isFunction(thing.splice)); +}; + +module.exports = isType; diff --git a/src/utils/isWindow.js b/src/utils/isWindow.js new file mode 100644 index 000000000..09b23006b --- /dev/null +++ b/src/utils/isWindow.js @@ -0,0 +1 @@ +module.exports = (thing) => !!(thing && thing.Window) && (thing instanceof thing.Window); diff --git a/src/utils/pointerUtils.js b/src/utils/pointerUtils.js new file mode 100644 index 000000000..4da182a21 --- /dev/null +++ b/src/utils/pointerUtils.js @@ -0,0 +1,232 @@ +const hypot = require('./hypot'); +const browser = require('./browser'); +const dom = require('./domObjects'); +const isType = require('./isType'); + +const pointerUtils = { + copyCoords: function (dest, src) { + dest.page = dest.page || {}; + dest.page.x = src.page.x; + dest.page.y = src.page.y; + + dest.client = dest.client || {}; + dest.client.x = src.client.x; + dest.client.y = src.client.y; + + dest.timeStamp = src.timeStamp; + }, + + setEventDeltas: function (targetObj, prev, cur) { + const now = new Date().getTime(); + + 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 = now - prev.timeStamp; + + // set pointer velocity + const dt = Math.max(targetObj.timeStamp / 1000, 0.001); + + targetObj.page.speed = hypot(targetObj.page.x, targetObj.page.y) / dt; + targetObj.page.vx = targetObj.page.x / dt; + targetObj.page.vy = targetObj.page.y / dt; + + targetObj.client.speed = hypot(targetObj.client.x, targetObj.page.y) / dt; + targetObj.client.vx = targetObj.client.x / dt; + targetObj.client.vy = targetObj.client.y / dt; + }, + + isNativePointer: function (pointer) { + return (pointer instanceof dom.Event || pointer instanceof dom.Touch); + }, + + // Get specified X/Y coords for mouse or event.touches[0] + getXY: function (type, pointer, xy) { + xy = xy || {}; + type = type || 'page'; + + xy.x = pointer[type + 'X']; + xy.y = pointer[type + 'Y']; + + return xy; + }, + + getPageXY: function (pointer, page) { + page = page || {}; + + // Opera Mobile handles the viewport and scrolling oddly + if (browser.isOperaMobile && pointerUtils.isNativePointer(pointer)) { + pointerUtils.getXY('screen', pointer, page); + + page.x += window.scrollX; + page.y += window.scrollY; + } + else { + pointerUtils.getXY('page', pointer, page); + } + + return page; + }, + + getClientXY: function (pointer, client) { + client = client || {}; + + if (browser.isOperaMobile && pointerUtils.isNativePointer(pointer)) { + // Opera Mobile handles the viewport and scrolling oddly + pointerUtils.getXY('screen', pointer, client); + } + else { + pointerUtils.getXY('client', pointer, client); + } + + return client; + }, + + getPointerId: function (pointer) { + return isType.isNumber(pointer.pointerId)? pointer.pointerId : pointer.identifier; + }, + + prefixedPropREs: { + webkit: /(Movement[XY]|Radius[XY]|RotationAngle|Force)$/, + }, + + pointerExtend: function (dest, source) { + for (const prop in source) { + const prefixedPropREs = pointerUtils.prefixedPropREs; + let deprecated = false; + + // skip deprecated prefixed properties + for (const vendor in prefixedPropREs) { + if (prop.indexOf(vendor) === 0 && prefixedPropREs[vendor].test(prop)) { + deprecated = true; + break; + } + } + + if (!deprecated) { + dest[prop] = source[prop]; + } + } + return dest; + }, + + getTouchPair: function (event) { + const touches = []; + + // array of touches is supplied + if (isType.isArray(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; + }, + + pointerAverage: function (pointers) { + const average = { + pageX : 0, + pageY : 0, + clientX: 0, + clientY: 0, + screenX: 0, + screenY: 0, + }; + + for (const pointer of pointers) { + for (const prop in average) { + average[prop] += pointer[prop]; + } + } + for (const prop in average) { + average[prop] /= pointers.length; + } + + return average; + }, + + touchBBox: function (event) { + if (!event.length && !(event.touches && event.touches.length > 1)) { + return; + } + + const touches = pointerUtils.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, + width: maxX - minX, + height: maxY - minY, + }; + }, + + touchDistance: function (event, deltaSource) { + deltaSource = deltaSource; + + const sourceX = deltaSource + 'X'; + const sourceY = deltaSource + 'Y'; + const touches = pointerUtils.getTouchPair(event); + + + const dx = touches[0][sourceX] - touches[1][sourceX]; + const dy = touches[0][sourceY] - touches[1][sourceY]; + + return hypot(dx, dy); + }, + + touchAngle: function (event, prevAngle, deltaSource) { + deltaSource = deltaSource; + + const sourceX = deltaSource + 'X'; + const sourceY = deltaSource + 'Y'; + const touches = pointerUtils.getTouchPair(event); + const dx = touches[0][sourceX] - touches[1][sourceX]; + const dy = touches[0][sourceY] - touches[1][sourceY]; + let angle = 180 * Math.atan(dy / dx) / Math.PI; + + if (isType.isNumber(prevAngle)) { + const dr = angle - prevAngle; + const drClamped = dr % 360; + + if (drClamped > 315) { + angle -= 360 + (angle / 360)|0 * 360; + } + else if (drClamped > 135) { + angle -= 180 + (angle / 360)|0 * 360; + } + else if (drClamped < -315) { + angle += 360 + (angle / 360)|0 * 360; + } + else if (drClamped < -135) { + angle += 180 + (angle / 360)|0 * 360; + } + } + + return angle; + }, +}; + +module.exports = pointerUtils; diff --git a/src/utils/raf.js b/src/utils/raf.js new file mode 100644 index 000000000..e3fc17cda --- /dev/null +++ b/src/utils/raf.js @@ -0,0 +1,32 @@ +const vendors = ['ms', 'moz', 'webkit', 'o']; +let lastTime = 0; +let request; +let cancel; + +for (let x = 0; x < vendors.length && !window.requestAnimationFrame; x++) { + request = window[vendors[x] + 'RequestAnimationFrame']; + cancel = window[vendors[x] +'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']; +} + +if (!request) { + request = function (callback) { + const currTime = new Date().getTime(); + const timeToCall = Math.max(0, 16 - (currTime - lastTime)); + const id = setTimeout(function () { callback(currTime + timeToCall); }, + timeToCall); + + lastTime = currTime + timeToCall; + return id; + }; +} + +if (!cancel) { + cancel = function (id) { + clearTimeout(id); + }; +} + +module.exports = { + request, + cancel, +}; diff --git a/src/utils/signals.js b/src/utils/signals.js new file mode 100644 index 000000000..c215e7353 --- /dev/null +++ b/src/utils/signals.js @@ -0,0 +1,39 @@ +const { indexOf } = require('./arr'); + +const listeners = { + // signalName: [listeners], +}; + +const signals = { + on: function (name, listener) { + if (!listeners[name]) { + listeners[name] = [listener]; + return; + } + + listeners[name].push(listener); + }, + off: function (name, listener) { + if (!listeners[name]) { return; } + + const index = indexOf(listeners[name], listener); + + if (index !== -1) { + listeners[name].splice(index, 1); + } + }, + fire: function (name, arg) { + const targetListeners = listeners[name]; + + if (!targetListeners) { return; } + + for (let i = 0; i < targetListeners.length; i++) { + if (targetListeners[i](arg, name) === false) { + return; + } + } + }, + listeners: listeners, +}; + +module.exports = signals; diff --git a/src/utils/window.js b/src/utils/window.js new file mode 100644 index 000000000..28c29f9b1 --- /dev/null +++ b/src/utils/window.js @@ -0,0 +1,42 @@ +const win = module.exports; +const isWindow = require('./isWindow'); + +function init (window) { + // get wrapped window if using Shadow DOM polyfill + + win.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) { + // return wrapped window + win.window = window.wrap(window); + } + + // no Shadow DOM polyfil or native implementation + win.window = window; +} + +if (typeof window === 'undefined') { + win.window = undefined; + win.realWindow = undefined; +} +else { + init(window); +} + +win.getWindow = function getWindow (node) { + if (isWindow(node)) { + return node; + } + + const rootNode = (node.ownerDocument || node); + + return rootNode.defaultView || rootNode.parentWindow || win.window; +}; + +win.init = init; diff --git a/test/fixtures/baseFixture.html b/test/fixtures/baseFixture.html new file mode 100644 index 000000000..281c6866c --- /dev/null +++ b/test/fixtures/baseFixture.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/test/data.js b/test/fixtures/data.js similarity index 100% rename from test/data.js rename to test/fixtures/data.js diff --git a/test/test.js b/test/test.js index 50d86ffa0..f8bd040bd 100644 --- a/test/test.js +++ b/test/test.js @@ -1,11 +1,16 @@ +var interact = require('../src/interact'); + +require('./fixtures/data'); + var expect = chai.expect, should = chai.should(), debug = interact.debug(), PointerEvent = window.PointerEvent || window.MSPointerEvent; -function blank () {} +function blank() { +} -function mockEvent (options, target, currentTarget) { +function mockEvent(options, target, currentTarget) { 'use strict'; options.target = options.target || target; @@ -17,8 +22,8 @@ function mockEvent (options, target, currentTarget) { type: options.type, pageX: options.x, pageY: options.y, - clientX: options.x - (options.scrollX|0), - clientY: options.y - (options.scrollY|0), + clientX: options.x - (options.scrollX | 0), + clientY: options.y - (options.scrollY | 0), touches: options.touches && options.touches.map(mockEvent), changedTouches: options.changed && options.changed.map(mockEvent), pointerId: options.pointerId || 0, @@ -33,15 +38,24 @@ function mockEvent (options, target, currentTarget) { describe('interact', function () { 'use strict'; + before(function () { + fixture.setBase('test/fixtures'); + fixture.load('baseFixture.html'); + }); + + afterEach(function () { + fixture.cleanup(); + }); + describe('when called as a function', function () { var validSelector = 'svg .draggable, body button'; it('should return an Interactable when given an Element', function () { - var bod = interact(document.body); + var el = interact(fixture.el); - expect(bod).to.be.an.instanceof(debug.Interactable); + expect(el).to.be.an.instanceof(debug.Interactable); - bod.element().should.equal(document.body); + el.element().should.equal(fixture.el); }); it('should return an Interactable when given a valid CSS selector string', function () { @@ -63,7 +77,8 @@ describe('interact', function () { error.should.be.instanceof(DOMException); }); - it('should return the same value from a given parameter unless returned Interactable is unset', function () { var iBody = interact(document.body), + it('should return the same value from a given parameter unless returned Interactable is unset', function () { + var iBody = interact(document.body), iSelector = interact(validSelector); interact(document.body).should.equal(iBody); @@ -88,14 +103,14 @@ describe('Interactable', function () { var defaults = debug.defaultOptions, iable = interact(document.createElement('div')), simpleOptions = { - draggable : 'draggable', - dropzone : 'dropzone', - resizable : 'resizable', - squareResize : 'squareResize', - gesturable : 'gesturable', - styleCursor : 'styleCursor', - origin : 'origin', - deltaSource : 'deltaSource' + draggable: 'draggable', + dropzone: 'dropzone', + resizable: 'resizable', + squareResize: 'squareResize', + gesturable: 'gesturable', + styleCursor: 'styleCursor', + origin: 'origin', + deltaSource: 'deltaSource' }, enableOptions = [ 'snap', @@ -143,19 +158,21 @@ describe('Interactable', function () { i, action, actions = ['drag', 'resizexy', 'resizex', 'resizey', 'gesture'], - returnActionI = function () { return actions[i]; }; + returnActionI = function () { + return actions[i]; + }; it('should set set the function used to determine actions on pointer down events', function () { iDiv.actionChecker(returnActionI); for (i = 0; action = actions[i], i < actions.length; i++) { - debug.pointerDown.call(div, mockEvent({ + debug.listeners.pointerDown.call(div, mockEvent({ target: div, pointerId: 1 })); if (PointerEvent && action === 'gesture') { - debug.pointerDown.call(div, mockEvent({ + debug.listeners.pointerDown.call(div, mockEvent({ target: div, pointerId: 2 })); @@ -186,21 +203,21 @@ describe('Events', function () { describe('drag sequence', function () { draggable.draggable({ - onstart: pushEvent, - onmove: pushEvent, - onend: pushEvent + onstart: pushEvent, + onmove: pushEvent, + onend: pushEvent }).actionChecker(function () { return 'drag'; }); - debug.pointerDown(mockEvents[0]); - debug.pointerMove(mockEvents[1]); - debug.pointerMove(mockEvents[2]); - debug.pointerUp (mockEvents[3]); + debug.listeners.pointerDown(mockEvents[0]); + debug.listeners.pointerMove(mockEvents[1]); + debug.listeners.pointerMove(mockEvents[2]); + debug.listeners.pointerUp(mockEvents[3]); it('should be triggered by mousedown -> mousemove -> mouseup sequence', function () { events.length.should.equal(4); - + events[0].type.should.equal('dragstart'); events[1].type.should.equal('dragmove'); events[2].type.should.equal('dragmove'); @@ -278,28 +295,33 @@ describe('Events', function () { onstart: pushEvent, onmove: pushEvent, onend: pushEvent - }).actionChecker(function () { return 'gesture'; }), + }).actionChecker(function () { + return 'gesture'; + }), mockEvents = data.touch2Move2End2.map(function (e) { return mockEvent(e, element); }), gestureEvents = [], - eventMap = [1, 2, 3, 4]; + eventMap = [1, 2, 3, 4], + debugRecord; // The pointers must be recorded here since event listeners // don't call the related functions. The recorded pointermove events // are used to calculate gesture angle, scale, etc. - debug.pointerDown(mockEvents[0]); - debug.pointerDown(mockEvents[1]); + debug.listeners.pointerDown(mockEvents[0]); + debug.listeners.pointerDown(mockEvents[1]); + + debugRecord = PointerEvent && debug.recordPointer || debug.recordTouches || debug.recordPointer; - debug[PointerEvent? 'recordPointers': 'recordTouches'](mockEvents[2]); - debug.pointerMove(mockEvents[2]); + debugRecord && debugRecord(mockEvents[2]); + debug.listeners.pointerMove(mockEvents[2]); - debug[PointerEvent? 'recordPointers': 'recordTouches'](mockEvents[3]); - debug.pointerMove(mockEvents[3]); + debugRecord && debugRecord(mockEvents[3]); + debug.listeners.pointerMove(mockEvents[3]); - debug.pointerUp(mockEvents[4]); - debug.pointerUp(mockEvents[5]); + debug.listeners.pointerUp(mockEvents[4]); + debug.listeners.pointerUp(mockEvents[5]); it('should be started by 2 touches starting and moving and end when there are fewer than two active touches', function () { gestureEvents.length.should.equal(4); @@ -337,12 +359,12 @@ describe('Events', function () { mEvent = mockEvents[eventMap[i]], gEvent = gestureEvents[i], i < gestureEvents.length; i++) { - var average = PointerEvent? mEvent: interact.getTouchAverage(mEvent), + var average = PointerEvent ? mEvent : interact.getTouchAverage(mEvent), coords = ['pageX', 'pageY', 'clientX', 'clientY']; - - coords.forEach(function (coord) { - gEvent[coord].should.equal(average[coord]); - }); + + coords.forEach(function (coord) { + gEvent[coord].should.equal(average[coord]); + }); } }); });