├── .codeclimate.yml ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── .testem.json ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── cycle-hammer.js └── cycle-hammer.min.js ├── doc └── api.md ├── examples ├── basic │ ├── bundle.js │ ├── index.html │ ├── index.js │ └── main.js ├── quadruple-tap │ ├── bundle.js │ ├── index.html │ ├── index.js │ └── main.js ├── recognizeWith-requireFailure-taps │ ├── bundle.js │ ├── index.html │ ├── index.js │ └── main.js ├── recognizeWith │ ├── bundle.js │ ├── index.html │ ├── index.js │ └── main.js └── vertical-pan │ ├── bundle.js │ ├── index.html │ ├── index.js │ └── main.js ├── lib ├── cycle-hammer.js ├── fromEvent.js ├── hammerDriver.js ├── makeHammerDriver.js └── standardEventTypes.js ├── package.json ├── scripts ├── build-examples.js ├── cyclic-docs-template.md.ejs └── make-api-doc.js ├── src ├── cycle-hammer.js ├── fromEvent.js ├── hammerDriver.js ├── makeHammerDriver.js └── standardEventTypes.js └── test ├── MockObservable.js ├── index.js ├── mockDomDriver.js └── page └── index.html /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | eslint: 4 | enabled: true 5 | csslint: 6 | enabled: false 7 | ratings: 8 | paths: 9 | - "**.js" 10 | - "**.jsx" 11 | exclude_paths: 12 | - examples/**/* 13 | - doc/* 14 | - scripts/* 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-cycle", 3 | "env": { 4 | "es6": true, 5 | "browser": true, 6 | "node": true 7 | }, 8 | "ecmaFeatures": { 9 | "modules": true 10 | }, 11 | "rules": { 12 | "new-cap": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | /node_modules 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.sublime-project 3 | *.sublime-workspace 4 | -------------------------------------------------------------------------------- /.testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "mocha+chai", 3 | "src_files": [ 4 | "test/index.js" 5 | ], 6 | "before_tests": "browserify test/index.js -t babelify -o test/bundle.js", 7 | "serve_files": [ 8 | "test/bundle.js" 9 | ], 10 | "after_tests": "rm test/bundle.js", 11 | "launch_in_ci": [ 12 | "firefox" 13 | ], 14 | "launch_in_dev": [ 15 | "chrome" 16 | ], 17 | "ignore_missing_launchers": true 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1" 4 | before_script: 5 | - "export DISPLAY=:99.0" 6 | - "sh -e /etc/init.d/xvfb start" 7 | - sleep 3 # give xvfb some time to start 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.1.1 (2015-11-25) 2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Cyclic Materials 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cycle-hammer-driver 2 | 3 | > A Hammer driver for Cycle.js. 4 | 5 | [![Cycle.js](https://img.shields.io/badge/Cycle.js-compatible-brightgreen.svg)](https://github.com/cyclejs) 6 | 7 | The driver incorporates the [Hammer.js](https://hammerjs.github.io/) 8 | gesture library. 9 | 10 | [Read the documentation](https://github.com/CyclicMaterials/cycle-hammer-driver/blob/master/doc/api.md) 11 | and check the [examples](https://github.com/CyclicMaterials/cycle-hammer-driver/blob/master/examples). 12 | 13 | ## License 14 | 15 | MIT © [Cyclic Materials](http://github.com/CyclicMaterials) 16 | 17 | - - - 18 | 19 | [![Build Status](https://travis-ci.org/CyclicMaterials/cycle-hammer-driver.svg)](https://travis-ci.org/CyclicMaterials/cycle-hammer-driver) 20 | [![Code Climate](https://codeclimate.com/github/CyclicMaterials/cycle-hammer-driver/badges/gpa.svg)](https://codeclimate.com/github/CyclicMaterials/cycle-hammer-driver) 21 | [![Dependency Status](https://david-dm.org/CyclicMaterials/cycle-hammer-driver.svg)](https://david-dm.org/CyclicMaterials/cycle-hammer-driver) 22 | [![devDependency Status](https://david-dm.org/CyclicMaterials/cycle-hammer-driver/dev-status.svg)](https://david-dm.org/CyclicMaterials/cycle-hammer-driver#info=devDependencies) 23 | -------------------------------------------------------------------------------- /dist/cycle-hammer.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.CycleHammer = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -1; 210 | } 211 | 212 | /** 213 | * split string on whitespace 214 | * @param {String} str 215 | * @returns {Array} words 216 | */ 217 | function splitStr(str) { 218 | return str.trim().split(/\s+/g); 219 | } 220 | 221 | /** 222 | * find if a array contains the object using indexOf or a simple polyFill 223 | * @param {Array} src 224 | * @param {String} find 225 | * @param {String} [findByKey] 226 | * @return {Boolean|Number} false when not found, or the index 227 | */ 228 | function inArray(src, find, findByKey) { 229 | if (src.indexOf && !findByKey) { 230 | return src.indexOf(find); 231 | } else { 232 | var i = 0; 233 | while (i < src.length) { 234 | if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { 235 | return i; 236 | } 237 | i++; 238 | } 239 | return -1; 240 | } 241 | } 242 | 243 | /** 244 | * convert array-like objects to real arrays 245 | * @param {Object} obj 246 | * @returns {Array} 247 | */ 248 | function toArray(obj) { 249 | return Array.prototype.slice.call(obj, 0); 250 | } 251 | 252 | /** 253 | * unique array with objects based on a key (like 'id') or just by the array's value 254 | * @param {Array} src [{id:1},{id:2},{id:1}] 255 | * @param {String} [key] 256 | * @param {Boolean} [sort=False] 257 | * @returns {Array} [{id:1},{id:2}] 258 | */ 259 | function uniqueArray(src, key, sort) { 260 | var results = []; 261 | var values = []; 262 | var i = 0; 263 | 264 | while (i < src.length) { 265 | var val = key ? src[i][key] : src[i]; 266 | if (inArray(values, val) < 0) { 267 | results.push(src[i]); 268 | } 269 | values[i] = val; 270 | i++; 271 | } 272 | 273 | if (sort) { 274 | if (!key) { 275 | results = results.sort(); 276 | } else { 277 | results = results.sort(function sortUniqueArray(a, b) { 278 | return a[key] > b[key]; 279 | }); 280 | } 281 | } 282 | 283 | return results; 284 | } 285 | 286 | /** 287 | * get the prefixed property 288 | * @param {Object} obj 289 | * @param {String} property 290 | * @returns {String|Undefined} prefixed 291 | */ 292 | function prefixed(obj, property) { 293 | var prefix, prop; 294 | var camelProp = property[0].toUpperCase() + property.slice(1); 295 | 296 | var i = 0; 297 | while (i < VENDOR_PREFIXES.length) { 298 | prefix = VENDOR_PREFIXES[i]; 299 | prop = (prefix) ? prefix + camelProp : property; 300 | 301 | if (prop in obj) { 302 | return prop; 303 | } 304 | i++; 305 | } 306 | return undefined; 307 | } 308 | 309 | /** 310 | * get a unique id 311 | * @returns {number} uniqueId 312 | */ 313 | var _uniqueId = 1; 314 | function uniqueId() { 315 | return _uniqueId++; 316 | } 317 | 318 | /** 319 | * get the window object of an element 320 | * @param {HTMLElement} element 321 | * @returns {DocumentView|Window} 322 | */ 323 | function getWindowForElement(element) { 324 | var doc = element.ownerDocument; 325 | return (doc.defaultView || doc.parentWindow); 326 | } 327 | 328 | var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; 329 | 330 | var SUPPORT_TOUCH = ('ontouchstart' in window); 331 | var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; 332 | var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); 333 | 334 | var INPUT_TYPE_TOUCH = 'touch'; 335 | var INPUT_TYPE_PEN = 'pen'; 336 | var INPUT_TYPE_MOUSE = 'mouse'; 337 | var INPUT_TYPE_KINECT = 'kinect'; 338 | 339 | var COMPUTE_INTERVAL = 25; 340 | 341 | var INPUT_START = 1; 342 | var INPUT_MOVE = 2; 343 | var INPUT_END = 4; 344 | var INPUT_CANCEL = 8; 345 | 346 | var DIRECTION_NONE = 1; 347 | var DIRECTION_LEFT = 2; 348 | var DIRECTION_RIGHT = 4; 349 | var DIRECTION_UP = 8; 350 | var DIRECTION_DOWN = 16; 351 | 352 | var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; 353 | var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; 354 | var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; 355 | 356 | var PROPS_XY = ['x', 'y']; 357 | var PROPS_CLIENT_XY = ['clientX', 'clientY']; 358 | 359 | /** 360 | * create new input type manager 361 | * @param {Manager} manager 362 | * @param {Function} callback 363 | * @returns {Input} 364 | * @constructor 365 | */ 366 | function Input(manager, callback) { 367 | var self = this; 368 | this.manager = manager; 369 | this.callback = callback; 370 | this.element = manager.element; 371 | this.target = manager.options.inputTarget; 372 | 373 | // smaller wrapper around the handler, for the scope and the enabled state of the manager, 374 | // so when disabled the input events are completely bypassed. 375 | this.domHandler = function(ev) { 376 | if (boolOrFn(manager.options.enable, [manager])) { 377 | self.handler(ev); 378 | } 379 | }; 380 | 381 | this.init(); 382 | 383 | } 384 | 385 | Input.prototype = { 386 | /** 387 | * should handle the inputEvent data and trigger the callback 388 | * @virtual 389 | */ 390 | handler: function() { }, 391 | 392 | /** 393 | * bind the events 394 | */ 395 | init: function() { 396 | this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); 397 | this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); 398 | this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); 399 | }, 400 | 401 | /** 402 | * unbind the events 403 | */ 404 | destroy: function() { 405 | this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); 406 | this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); 407 | this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); 408 | } 409 | }; 410 | 411 | /** 412 | * create new input type manager 413 | * called by the Manager constructor 414 | * @param {Hammer} manager 415 | * @returns {Input} 416 | */ 417 | function createInputInstance(manager) { 418 | var Type; 419 | var inputClass = manager.options.inputClass; 420 | 421 | if (inputClass) { 422 | Type = inputClass; 423 | } else if (SUPPORT_POINTER_EVENTS) { 424 | Type = PointerEventInput; 425 | } else if (SUPPORT_ONLY_TOUCH) { 426 | Type = TouchInput; 427 | } else if (!SUPPORT_TOUCH) { 428 | Type = MouseInput; 429 | } else { 430 | Type = TouchMouseInput; 431 | } 432 | return new (Type)(manager, inputHandler); 433 | } 434 | 435 | /** 436 | * handle input events 437 | * @param {Manager} manager 438 | * @param {String} eventType 439 | * @param {Object} input 440 | */ 441 | function inputHandler(manager, eventType, input) { 442 | var pointersLen = input.pointers.length; 443 | var changedPointersLen = input.changedPointers.length; 444 | var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); 445 | var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); 446 | 447 | input.isFirst = !!isFirst; 448 | input.isFinal = !!isFinal; 449 | 450 | if (isFirst) { 451 | manager.session = {}; 452 | } 453 | 454 | // source event is the normalized value of the domEvents 455 | // like 'touchstart, mouseup, pointerdown' 456 | input.eventType = eventType; 457 | 458 | // compute scale, rotation etc 459 | computeInputData(manager, input); 460 | 461 | // emit secret event 462 | manager.emit('hammer.input', input); 463 | 464 | manager.recognize(input); 465 | manager.session.prevInput = input; 466 | } 467 | 468 | /** 469 | * extend the data with some usable properties like scale, rotate, velocity etc 470 | * @param {Object} manager 471 | * @param {Object} input 472 | */ 473 | function computeInputData(manager, input) { 474 | var session = manager.session; 475 | var pointers = input.pointers; 476 | var pointersLength = pointers.length; 477 | 478 | // store the first input to calculate the distance and direction 479 | if (!session.firstInput) { 480 | session.firstInput = simpleCloneInputData(input); 481 | } 482 | 483 | // to compute scale and rotation we need to store the multiple touches 484 | if (pointersLength > 1 && !session.firstMultiple) { 485 | session.firstMultiple = simpleCloneInputData(input); 486 | } else if (pointersLength === 1) { 487 | session.firstMultiple = false; 488 | } 489 | 490 | var firstInput = session.firstInput; 491 | var firstMultiple = session.firstMultiple; 492 | var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; 493 | 494 | var center = input.center = getCenter(pointers); 495 | input.timeStamp = now(); 496 | input.deltaTime = input.timeStamp - firstInput.timeStamp; 497 | 498 | input.angle = getAngle(offsetCenter, center); 499 | input.distance = getDistance(offsetCenter, center); 500 | 501 | computeDeltaXY(session, input); 502 | input.offsetDirection = getDirection(input.deltaX, input.deltaY); 503 | 504 | input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; 505 | input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; 506 | 507 | computeIntervalInputData(session, input); 508 | 509 | // find the correct target 510 | var target = manager.element; 511 | if (hasParent(input.srcEvent.target, target)) { 512 | target = input.srcEvent.target; 513 | } 514 | input.target = target; 515 | } 516 | 517 | function computeDeltaXY(session, input) { 518 | var center = input.center; 519 | var offset = session.offsetDelta || {}; 520 | var prevDelta = session.prevDelta || {}; 521 | var prevInput = session.prevInput || {}; 522 | 523 | if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { 524 | prevDelta = session.prevDelta = { 525 | x: prevInput.deltaX || 0, 526 | y: prevInput.deltaY || 0 527 | }; 528 | 529 | offset = session.offsetDelta = { 530 | x: center.x, 531 | y: center.y 532 | }; 533 | } 534 | 535 | input.deltaX = prevDelta.x + (center.x - offset.x); 536 | input.deltaY = prevDelta.y + (center.y - offset.y); 537 | } 538 | 539 | /** 540 | * velocity is calculated every x ms 541 | * @param {Object} session 542 | * @param {Object} input 543 | */ 544 | function computeIntervalInputData(session, input) { 545 | var last = session.lastInterval || input, 546 | deltaTime = input.timeStamp - last.timeStamp, 547 | velocity, velocityX, velocityY, direction; 548 | 549 | if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { 550 | var deltaX = last.deltaX - input.deltaX; 551 | var deltaY = last.deltaY - input.deltaY; 552 | 553 | var v = getVelocity(deltaTime, deltaX, deltaY); 554 | velocityX = v.x; 555 | velocityY = v.y; 556 | velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; 557 | direction = getDirection(deltaX, deltaY); 558 | 559 | session.lastInterval = input; 560 | } else { 561 | // use latest velocity info if it doesn't overtake a minimum period 562 | velocity = last.velocity; 563 | velocityX = last.velocityX; 564 | velocityY = last.velocityY; 565 | direction = last.direction; 566 | } 567 | 568 | input.velocity = velocity; 569 | input.velocityX = velocityX; 570 | input.velocityY = velocityY; 571 | input.direction = direction; 572 | } 573 | 574 | /** 575 | * create a simple clone from the input used for storage of firstInput and firstMultiple 576 | * @param {Object} input 577 | * @returns {Object} clonedInputData 578 | */ 579 | function simpleCloneInputData(input) { 580 | // make a simple copy of the pointers because we will get a reference if we don't 581 | // we only need clientXY for the calculations 582 | var pointers = []; 583 | var i = 0; 584 | while (i < input.pointers.length) { 585 | pointers[i] = { 586 | clientX: round(input.pointers[i].clientX), 587 | clientY: round(input.pointers[i].clientY) 588 | }; 589 | i++; 590 | } 591 | 592 | return { 593 | timeStamp: now(), 594 | pointers: pointers, 595 | center: getCenter(pointers), 596 | deltaX: input.deltaX, 597 | deltaY: input.deltaY 598 | }; 599 | } 600 | 601 | /** 602 | * get the center of all the pointers 603 | * @param {Array} pointers 604 | * @return {Object} center contains `x` and `y` properties 605 | */ 606 | function getCenter(pointers) { 607 | var pointersLength = pointers.length; 608 | 609 | // no need to loop when only one touch 610 | if (pointersLength === 1) { 611 | return { 612 | x: round(pointers[0].clientX), 613 | y: round(pointers[0].clientY) 614 | }; 615 | } 616 | 617 | var x = 0, y = 0, i = 0; 618 | while (i < pointersLength) { 619 | x += pointers[i].clientX; 620 | y += pointers[i].clientY; 621 | i++; 622 | } 623 | 624 | return { 625 | x: round(x / pointersLength), 626 | y: round(y / pointersLength) 627 | }; 628 | } 629 | 630 | /** 631 | * calculate the velocity between two points. unit is in px per ms. 632 | * @param {Number} deltaTime 633 | * @param {Number} x 634 | * @param {Number} y 635 | * @return {Object} velocity `x` and `y` 636 | */ 637 | function getVelocity(deltaTime, x, y) { 638 | return { 639 | x: x / deltaTime || 0, 640 | y: y / deltaTime || 0 641 | }; 642 | } 643 | 644 | /** 645 | * get the direction between two points 646 | * @param {Number} x 647 | * @param {Number} y 648 | * @return {Number} direction 649 | */ 650 | function getDirection(x, y) { 651 | if (x === y) { 652 | return DIRECTION_NONE; 653 | } 654 | 655 | if (abs(x) >= abs(y)) { 656 | return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; 657 | } 658 | return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; 659 | } 660 | 661 | /** 662 | * calculate the absolute distance between two points 663 | * @param {Object} p1 {x, y} 664 | * @param {Object} p2 {x, y} 665 | * @param {Array} [props] containing x and y keys 666 | * @return {Number} distance 667 | */ 668 | function getDistance(p1, p2, props) { 669 | if (!props) { 670 | props = PROPS_XY; 671 | } 672 | var x = p2[props[0]] - p1[props[0]], 673 | y = p2[props[1]] - p1[props[1]]; 674 | 675 | return Math.sqrt((x * x) + (y * y)); 676 | } 677 | 678 | /** 679 | * calculate the angle between two coordinates 680 | * @param {Object} p1 681 | * @param {Object} p2 682 | * @param {Array} [props] containing x and y keys 683 | * @return {Number} angle 684 | */ 685 | function getAngle(p1, p2, props) { 686 | if (!props) { 687 | props = PROPS_XY; 688 | } 689 | var x = p2[props[0]] - p1[props[0]], 690 | y = p2[props[1]] - p1[props[1]]; 691 | return Math.atan2(y, x) * 180 / Math.PI; 692 | } 693 | 694 | /** 695 | * calculate the rotation degrees between two pointersets 696 | * @param {Array} start array of pointers 697 | * @param {Array} end array of pointers 698 | * @return {Number} rotation 699 | */ 700 | function getRotation(start, end) { 701 | return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); 702 | } 703 | 704 | /** 705 | * calculate the scale factor between two pointersets 706 | * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out 707 | * @param {Array} start array of pointers 708 | * @param {Array} end array of pointers 709 | * @return {Number} scale 710 | */ 711 | function getScale(start, end) { 712 | return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); 713 | } 714 | 715 | var MOUSE_INPUT_MAP = { 716 | mousedown: INPUT_START, 717 | mousemove: INPUT_MOVE, 718 | mouseup: INPUT_END 719 | }; 720 | 721 | var MOUSE_ELEMENT_EVENTS = 'mousedown'; 722 | var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; 723 | 724 | /** 725 | * Mouse events input 726 | * @constructor 727 | * @extends Input 728 | */ 729 | function MouseInput() { 730 | this.evEl = MOUSE_ELEMENT_EVENTS; 731 | this.evWin = MOUSE_WINDOW_EVENTS; 732 | 733 | this.allow = true; // used by Input.TouchMouse to disable mouse events 734 | this.pressed = false; // mousedown state 735 | 736 | Input.apply(this, arguments); 737 | } 738 | 739 | inherit(MouseInput, Input, { 740 | /** 741 | * handle mouse events 742 | * @param {Object} ev 743 | */ 744 | handler: function MEhandler(ev) { 745 | var eventType = MOUSE_INPUT_MAP[ev.type]; 746 | 747 | // on start we want to have the left mouse button down 748 | if (eventType & INPUT_START && ev.button === 0) { 749 | this.pressed = true; 750 | } 751 | 752 | if (eventType & INPUT_MOVE && ev.which !== 1) { 753 | eventType = INPUT_END; 754 | } 755 | 756 | // mouse must be down, and mouse events are allowed (see the TouchMouse input) 757 | if (!this.pressed || !this.allow) { 758 | return; 759 | } 760 | 761 | if (eventType & INPUT_END) { 762 | this.pressed = false; 763 | } 764 | 765 | this.callback(this.manager, eventType, { 766 | pointers: [ev], 767 | changedPointers: [ev], 768 | pointerType: INPUT_TYPE_MOUSE, 769 | srcEvent: ev 770 | }); 771 | } 772 | }); 773 | 774 | var POINTER_INPUT_MAP = { 775 | pointerdown: INPUT_START, 776 | pointermove: INPUT_MOVE, 777 | pointerup: INPUT_END, 778 | pointercancel: INPUT_CANCEL, 779 | pointerout: INPUT_CANCEL 780 | }; 781 | 782 | // in IE10 the pointer types is defined as an enum 783 | var IE10_POINTER_TYPE_ENUM = { 784 | 2: INPUT_TYPE_TOUCH, 785 | 3: INPUT_TYPE_PEN, 786 | 4: INPUT_TYPE_MOUSE, 787 | 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 788 | }; 789 | 790 | var POINTER_ELEMENT_EVENTS = 'pointerdown'; 791 | var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; 792 | 793 | // IE10 has prefixed support, and case-sensitive 794 | if (window.MSPointerEvent) { 795 | POINTER_ELEMENT_EVENTS = 'MSPointerDown'; 796 | POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; 797 | } 798 | 799 | /** 800 | * Pointer events input 801 | * @constructor 802 | * @extends Input 803 | */ 804 | function PointerEventInput() { 805 | this.evEl = POINTER_ELEMENT_EVENTS; 806 | this.evWin = POINTER_WINDOW_EVENTS; 807 | 808 | Input.apply(this, arguments); 809 | 810 | this.store = (this.manager.session.pointerEvents = []); 811 | } 812 | 813 | inherit(PointerEventInput, Input, { 814 | /** 815 | * handle mouse events 816 | * @param {Object} ev 817 | */ 818 | handler: function PEhandler(ev) { 819 | var store = this.store; 820 | var removePointer = false; 821 | 822 | var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); 823 | var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; 824 | var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; 825 | 826 | var isTouch = (pointerType == INPUT_TYPE_TOUCH); 827 | 828 | // get index of the event in the store 829 | var storeIndex = inArray(store, ev.pointerId, 'pointerId'); 830 | 831 | // start and mouse must be down 832 | if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { 833 | if (storeIndex < 0) { 834 | store.push(ev); 835 | storeIndex = store.length - 1; 836 | } 837 | } else if (eventType & (INPUT_END | INPUT_CANCEL)) { 838 | removePointer = true; 839 | } 840 | 841 | // it not found, so the pointer hasn't been down (so it's probably a hover) 842 | if (storeIndex < 0) { 843 | return; 844 | } 845 | 846 | // update the event in the store 847 | store[storeIndex] = ev; 848 | 849 | this.callback(this.manager, eventType, { 850 | pointers: store, 851 | changedPointers: [ev], 852 | pointerType: pointerType, 853 | srcEvent: ev 854 | }); 855 | 856 | if (removePointer) { 857 | // remove from the store 858 | store.splice(storeIndex, 1); 859 | } 860 | } 861 | }); 862 | 863 | var SINGLE_TOUCH_INPUT_MAP = { 864 | touchstart: INPUT_START, 865 | touchmove: INPUT_MOVE, 866 | touchend: INPUT_END, 867 | touchcancel: INPUT_CANCEL 868 | }; 869 | 870 | var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; 871 | var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; 872 | 873 | /** 874 | * Touch events input 875 | * @constructor 876 | * @extends Input 877 | */ 878 | function SingleTouchInput() { 879 | this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; 880 | this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; 881 | this.started = false; 882 | 883 | Input.apply(this, arguments); 884 | } 885 | 886 | inherit(SingleTouchInput, Input, { 887 | handler: function TEhandler(ev) { 888 | var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; 889 | 890 | // should we handle the touch events? 891 | if (type === INPUT_START) { 892 | this.started = true; 893 | } 894 | 895 | if (!this.started) { 896 | return; 897 | } 898 | 899 | var touches = normalizeSingleTouches.call(this, ev, type); 900 | 901 | // when done, reset the started state 902 | if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { 903 | this.started = false; 904 | } 905 | 906 | this.callback(this.manager, type, { 907 | pointers: touches[0], 908 | changedPointers: touches[1], 909 | pointerType: INPUT_TYPE_TOUCH, 910 | srcEvent: ev 911 | }); 912 | } 913 | }); 914 | 915 | /** 916 | * @this {TouchInput} 917 | * @param {Object} ev 918 | * @param {Number} type flag 919 | * @returns {undefined|Array} [all, changed] 920 | */ 921 | function normalizeSingleTouches(ev, type) { 922 | var all = toArray(ev.touches); 923 | var changed = toArray(ev.changedTouches); 924 | 925 | if (type & (INPUT_END | INPUT_CANCEL)) { 926 | all = uniqueArray(all.concat(changed), 'identifier', true); 927 | } 928 | 929 | return [all, changed]; 930 | } 931 | 932 | var TOUCH_INPUT_MAP = { 933 | touchstart: INPUT_START, 934 | touchmove: INPUT_MOVE, 935 | touchend: INPUT_END, 936 | touchcancel: INPUT_CANCEL 937 | }; 938 | 939 | var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; 940 | 941 | /** 942 | * Multi-user touch events input 943 | * @constructor 944 | * @extends Input 945 | */ 946 | function TouchInput() { 947 | this.evTarget = TOUCH_TARGET_EVENTS; 948 | this.targetIds = {}; 949 | 950 | Input.apply(this, arguments); 951 | } 952 | 953 | inherit(TouchInput, Input, { 954 | handler: function MTEhandler(ev) { 955 | var type = TOUCH_INPUT_MAP[ev.type]; 956 | var touches = getTouches.call(this, ev, type); 957 | if (!touches) { 958 | return; 959 | } 960 | 961 | this.callback(this.manager, type, { 962 | pointers: touches[0], 963 | changedPointers: touches[1], 964 | pointerType: INPUT_TYPE_TOUCH, 965 | srcEvent: ev 966 | }); 967 | } 968 | }); 969 | 970 | /** 971 | * @this {TouchInput} 972 | * @param {Object} ev 973 | * @param {Number} type flag 974 | * @returns {undefined|Array} [all, changed] 975 | */ 976 | function getTouches(ev, type) { 977 | var allTouches = toArray(ev.touches); 978 | var targetIds = this.targetIds; 979 | 980 | // when there is only one touch, the process can be simplified 981 | if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) { 982 | targetIds[allTouches[0].identifier] = true; 983 | return [allTouches, allTouches]; 984 | } 985 | 986 | var i, 987 | targetTouches, 988 | changedTouches = toArray(ev.changedTouches), 989 | changedTargetTouches = [], 990 | target = this.target; 991 | 992 | // get target touches from touches 993 | targetTouches = allTouches.filter(function(touch) { 994 | return hasParent(touch.target, target); 995 | }); 996 | 997 | // collect touches 998 | if (type === INPUT_START) { 999 | i = 0; 1000 | while (i < targetTouches.length) { 1001 | targetIds[targetTouches[i].identifier] = true; 1002 | i++; 1003 | } 1004 | } 1005 | 1006 | // filter changed touches to only contain touches that exist in the collected target ids 1007 | i = 0; 1008 | while (i < changedTouches.length) { 1009 | if (targetIds[changedTouches[i].identifier]) { 1010 | changedTargetTouches.push(changedTouches[i]); 1011 | } 1012 | 1013 | // cleanup removed touches 1014 | if (type & (INPUT_END | INPUT_CANCEL)) { 1015 | delete targetIds[changedTouches[i].identifier]; 1016 | } 1017 | i++; 1018 | } 1019 | 1020 | if (!changedTargetTouches.length) { 1021 | return; 1022 | } 1023 | 1024 | return [ 1025 | // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' 1026 | uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), 1027 | changedTargetTouches 1028 | ]; 1029 | } 1030 | 1031 | /** 1032 | * Combined touch and mouse input 1033 | * 1034 | * Touch has a higher priority then mouse, and while touching no mouse events are allowed. 1035 | * This because touch devices also emit mouse events while doing a touch. 1036 | * 1037 | * @constructor 1038 | * @extends Input 1039 | */ 1040 | function TouchMouseInput() { 1041 | Input.apply(this, arguments); 1042 | 1043 | var handler = bindFn(this.handler, this); 1044 | this.touch = new TouchInput(this.manager, handler); 1045 | this.mouse = new MouseInput(this.manager, handler); 1046 | } 1047 | 1048 | inherit(TouchMouseInput, Input, { 1049 | /** 1050 | * handle mouse and touch events 1051 | * @param {Hammer} manager 1052 | * @param {String} inputEvent 1053 | * @param {Object} inputData 1054 | */ 1055 | handler: function TMEhandler(manager, inputEvent, inputData) { 1056 | var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), 1057 | isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); 1058 | 1059 | // when we're in a touch event, so block all upcoming mouse events 1060 | // most mobile browser also emit mouseevents, right after touchstart 1061 | if (isTouch) { 1062 | this.mouse.allow = false; 1063 | } else if (isMouse && !this.mouse.allow) { 1064 | return; 1065 | } 1066 | 1067 | // reset the allowMouse when we're done 1068 | if (inputEvent & (INPUT_END | INPUT_CANCEL)) { 1069 | this.mouse.allow = true; 1070 | } 1071 | 1072 | this.callback(manager, inputEvent, inputData); 1073 | }, 1074 | 1075 | /** 1076 | * remove the event listeners 1077 | */ 1078 | destroy: function destroy() { 1079 | this.touch.destroy(); 1080 | this.mouse.destroy(); 1081 | } 1082 | }); 1083 | 1084 | var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); 1085 | var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; 1086 | 1087 | // magical touchAction value 1088 | var TOUCH_ACTION_COMPUTE = 'compute'; 1089 | var TOUCH_ACTION_AUTO = 'auto'; 1090 | var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented 1091 | var TOUCH_ACTION_NONE = 'none'; 1092 | var TOUCH_ACTION_PAN_X = 'pan-x'; 1093 | var TOUCH_ACTION_PAN_Y = 'pan-y'; 1094 | 1095 | /** 1096 | * Touch Action 1097 | * sets the touchAction property or uses the js alternative 1098 | * @param {Manager} manager 1099 | * @param {String} value 1100 | * @constructor 1101 | */ 1102 | function TouchAction(manager, value) { 1103 | this.manager = manager; 1104 | this.set(value); 1105 | } 1106 | 1107 | TouchAction.prototype = { 1108 | /** 1109 | * set the touchAction value on the element or enable the polyfill 1110 | * @param {String} value 1111 | */ 1112 | set: function(value) { 1113 | // find out the touch-action by the event handlers 1114 | if (value == TOUCH_ACTION_COMPUTE) { 1115 | value = this.compute(); 1116 | } 1117 | 1118 | if (NATIVE_TOUCH_ACTION) { 1119 | this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; 1120 | } 1121 | this.actions = value.toLowerCase().trim(); 1122 | }, 1123 | 1124 | /** 1125 | * just re-set the touchAction value 1126 | */ 1127 | update: function() { 1128 | this.set(this.manager.options.touchAction); 1129 | }, 1130 | 1131 | /** 1132 | * compute the value for the touchAction property based on the recognizer's settings 1133 | * @returns {String} value 1134 | */ 1135 | compute: function() { 1136 | var actions = []; 1137 | each(this.manager.recognizers, function(recognizer) { 1138 | if (boolOrFn(recognizer.options.enable, [recognizer])) { 1139 | actions = actions.concat(recognizer.getTouchAction()); 1140 | } 1141 | }); 1142 | return cleanTouchActions(actions.join(' ')); 1143 | }, 1144 | 1145 | /** 1146 | * this method is called on each input cycle and provides the preventing of the browser behavior 1147 | * @param {Object} input 1148 | */ 1149 | preventDefaults: function(input) { 1150 | // not needed with native support for the touchAction property 1151 | if (NATIVE_TOUCH_ACTION) { 1152 | return; 1153 | } 1154 | 1155 | var srcEvent = input.srcEvent; 1156 | var direction = input.offsetDirection; 1157 | 1158 | // if the touch action did prevented once this session 1159 | if (this.manager.session.prevented) { 1160 | srcEvent.preventDefault(); 1161 | return; 1162 | } 1163 | 1164 | var actions = this.actions; 1165 | var hasNone = inStr(actions, TOUCH_ACTION_NONE); 1166 | var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); 1167 | var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); 1168 | 1169 | if (hasNone || 1170 | (hasPanY && direction & DIRECTION_HORIZONTAL) || 1171 | (hasPanX && direction & DIRECTION_VERTICAL)) { 1172 | return this.preventSrc(srcEvent); 1173 | } 1174 | }, 1175 | 1176 | /** 1177 | * call preventDefault to prevent the browser's default behavior (scrolling in most cases) 1178 | * @param {Object} srcEvent 1179 | */ 1180 | preventSrc: function(srcEvent) { 1181 | this.manager.session.prevented = true; 1182 | srcEvent.preventDefault(); 1183 | } 1184 | }; 1185 | 1186 | /** 1187 | * when the touchActions are collected they are not a valid value, so we need to clean things up. * 1188 | * @param {String} actions 1189 | * @returns {*} 1190 | */ 1191 | function cleanTouchActions(actions) { 1192 | // none 1193 | if (inStr(actions, TOUCH_ACTION_NONE)) { 1194 | return TOUCH_ACTION_NONE; 1195 | } 1196 | 1197 | var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); 1198 | var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); 1199 | 1200 | // pan-x and pan-y can be combined 1201 | if (hasPanX && hasPanY) { 1202 | return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; 1203 | } 1204 | 1205 | // pan-x OR pan-y 1206 | if (hasPanX || hasPanY) { 1207 | return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; 1208 | } 1209 | 1210 | // manipulation 1211 | if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { 1212 | return TOUCH_ACTION_MANIPULATION; 1213 | } 1214 | 1215 | return TOUCH_ACTION_AUTO; 1216 | } 1217 | 1218 | /** 1219 | * Recognizer flow explained; * 1220 | * All recognizers have the initial state of POSSIBLE when a input session starts. 1221 | * The definition of a input session is from the first input until the last input, with all it's movement in it. * 1222 | * Example session for mouse-input: mousedown -> mousemove -> mouseup 1223 | * 1224 | * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed 1225 | * which determines with state it should be. 1226 | * 1227 | * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to 1228 | * POSSIBLE to give it another change on the next cycle. 1229 | * 1230 | * Possible 1231 | * | 1232 | * +-----+---------------+ 1233 | * | | 1234 | * +-----+-----+ | 1235 | * | | | 1236 | * Failed Cancelled | 1237 | * +-------+------+ 1238 | * | | 1239 | * Recognized Began 1240 | * | 1241 | * Changed 1242 | * | 1243 | * Ended/Recognized 1244 | */ 1245 | var STATE_POSSIBLE = 1; 1246 | var STATE_BEGAN = 2; 1247 | var STATE_CHANGED = 4; 1248 | var STATE_ENDED = 8; 1249 | var STATE_RECOGNIZED = STATE_ENDED; 1250 | var STATE_CANCELLED = 16; 1251 | var STATE_FAILED = 32; 1252 | 1253 | /** 1254 | * Recognizer 1255 | * Every recognizer needs to extend from this class. 1256 | * @constructor 1257 | * @param {Object} options 1258 | */ 1259 | function Recognizer(options) { 1260 | this.id = uniqueId(); 1261 | 1262 | this.manager = null; 1263 | this.options = merge(options || {}, this.defaults); 1264 | 1265 | // default is enable true 1266 | this.options.enable = ifUndefined(this.options.enable, true); 1267 | 1268 | this.state = STATE_POSSIBLE; 1269 | 1270 | this.simultaneous = {}; 1271 | this.requireFail = []; 1272 | } 1273 | 1274 | Recognizer.prototype = { 1275 | /** 1276 | * @virtual 1277 | * @type {Object} 1278 | */ 1279 | defaults: {}, 1280 | 1281 | /** 1282 | * set options 1283 | * @param {Object} options 1284 | * @return {Recognizer} 1285 | */ 1286 | set: function(options) { 1287 | extend(this.options, options); 1288 | 1289 | // also update the touchAction, in case something changed about the directions/enabled state 1290 | this.manager && this.manager.touchAction.update(); 1291 | return this; 1292 | }, 1293 | 1294 | /** 1295 | * recognize simultaneous with an other recognizer. 1296 | * @param {Recognizer} otherRecognizer 1297 | * @returns {Recognizer} this 1298 | */ 1299 | recognizeWith: function(otherRecognizer) { 1300 | if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { 1301 | return this; 1302 | } 1303 | 1304 | var simultaneous = this.simultaneous; 1305 | otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); 1306 | if (!simultaneous[otherRecognizer.id]) { 1307 | simultaneous[otherRecognizer.id] = otherRecognizer; 1308 | otherRecognizer.recognizeWith(this); 1309 | } 1310 | return this; 1311 | }, 1312 | 1313 | /** 1314 | * drop the simultaneous link. it doesnt remove the link on the other recognizer. 1315 | * @param {Recognizer} otherRecognizer 1316 | * @returns {Recognizer} this 1317 | */ 1318 | dropRecognizeWith: function(otherRecognizer) { 1319 | if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { 1320 | return this; 1321 | } 1322 | 1323 | otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); 1324 | delete this.simultaneous[otherRecognizer.id]; 1325 | return this; 1326 | }, 1327 | 1328 | /** 1329 | * recognizer can only run when an other is failing 1330 | * @param {Recognizer} otherRecognizer 1331 | * @returns {Recognizer} this 1332 | */ 1333 | requireFailure: function(otherRecognizer) { 1334 | if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { 1335 | return this; 1336 | } 1337 | 1338 | var requireFail = this.requireFail; 1339 | otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); 1340 | if (inArray(requireFail, otherRecognizer) === -1) { 1341 | requireFail.push(otherRecognizer); 1342 | otherRecognizer.requireFailure(this); 1343 | } 1344 | return this; 1345 | }, 1346 | 1347 | /** 1348 | * drop the requireFailure link. it does not remove the link on the other recognizer. 1349 | * @param {Recognizer} otherRecognizer 1350 | * @returns {Recognizer} this 1351 | */ 1352 | dropRequireFailure: function(otherRecognizer) { 1353 | if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) { 1354 | return this; 1355 | } 1356 | 1357 | otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); 1358 | var index = inArray(this.requireFail, otherRecognizer); 1359 | if (index > -1) { 1360 | this.requireFail.splice(index, 1); 1361 | } 1362 | return this; 1363 | }, 1364 | 1365 | /** 1366 | * has require failures boolean 1367 | * @returns {boolean} 1368 | */ 1369 | hasRequireFailures: function() { 1370 | return this.requireFail.length > 0; 1371 | }, 1372 | 1373 | /** 1374 | * if the recognizer can recognize simultaneous with an other recognizer 1375 | * @param {Recognizer} otherRecognizer 1376 | * @returns {Boolean} 1377 | */ 1378 | canRecognizeWith: function(otherRecognizer) { 1379 | return !!this.simultaneous[otherRecognizer.id]; 1380 | }, 1381 | 1382 | /** 1383 | * You should use `tryEmit` instead of `emit` directly to check 1384 | * that all the needed recognizers has failed before emitting. 1385 | * @param {Object} input 1386 | */ 1387 | emit: function(input) { 1388 | var self = this; 1389 | var state = this.state; 1390 | 1391 | function emit(withState) { 1392 | self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); 1393 | } 1394 | 1395 | // 'panstart' and 'panmove' 1396 | if (state < STATE_ENDED) { 1397 | emit(true); 1398 | } 1399 | 1400 | emit(); // simple 'eventName' events 1401 | 1402 | // panend and pancancel 1403 | if (state >= STATE_ENDED) { 1404 | emit(true); 1405 | } 1406 | }, 1407 | 1408 | /** 1409 | * Check that all the require failure recognizers has failed, 1410 | * if true, it emits a gesture event, 1411 | * otherwise, setup the state to FAILED. 1412 | * @param {Object} input 1413 | */ 1414 | tryEmit: function(input) { 1415 | if (this.canEmit()) { 1416 | return this.emit(input); 1417 | } 1418 | // it's failing anyway 1419 | this.state = STATE_FAILED; 1420 | }, 1421 | 1422 | /** 1423 | * can we emit? 1424 | * @returns {boolean} 1425 | */ 1426 | canEmit: function() { 1427 | var i = 0; 1428 | while (i < this.requireFail.length) { 1429 | if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { 1430 | return false; 1431 | } 1432 | i++; 1433 | } 1434 | return true; 1435 | }, 1436 | 1437 | /** 1438 | * update the recognizer 1439 | * @param {Object} inputData 1440 | */ 1441 | recognize: function(inputData) { 1442 | // make a new copy of the inputData 1443 | // so we can change the inputData without messing up the other recognizers 1444 | var inputDataClone = extend({}, inputData); 1445 | 1446 | // is is enabled and allow recognizing? 1447 | if (!boolOrFn(this.options.enable, [this, inputDataClone])) { 1448 | this.reset(); 1449 | this.state = STATE_FAILED; 1450 | return; 1451 | } 1452 | 1453 | // reset when we've reached the end 1454 | if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { 1455 | this.state = STATE_POSSIBLE; 1456 | } 1457 | 1458 | this.state = this.process(inputDataClone); 1459 | 1460 | // the recognizer has recognized a gesture 1461 | // so trigger an event 1462 | if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { 1463 | this.tryEmit(inputDataClone); 1464 | } 1465 | }, 1466 | 1467 | /** 1468 | * return the state of the recognizer 1469 | * the actual recognizing happens in this method 1470 | * @virtual 1471 | * @param {Object} inputData 1472 | * @returns {Const} STATE 1473 | */ 1474 | process: function(inputData) { }, // jshint ignore:line 1475 | 1476 | /** 1477 | * return the preferred touch-action 1478 | * @virtual 1479 | * @returns {Array} 1480 | */ 1481 | getTouchAction: function() { }, 1482 | 1483 | /** 1484 | * called when the gesture isn't allowed to recognize 1485 | * like when another is being recognized or it is disabled 1486 | * @virtual 1487 | */ 1488 | reset: function() { } 1489 | }; 1490 | 1491 | /** 1492 | * get a usable string, used as event postfix 1493 | * @param {Const} state 1494 | * @returns {String} state 1495 | */ 1496 | function stateStr(state) { 1497 | if (state & STATE_CANCELLED) { 1498 | return 'cancel'; 1499 | } else if (state & STATE_ENDED) { 1500 | return 'end'; 1501 | } else if (state & STATE_CHANGED) { 1502 | return 'move'; 1503 | } else if (state & STATE_BEGAN) { 1504 | return 'start'; 1505 | } 1506 | return ''; 1507 | } 1508 | 1509 | /** 1510 | * direction cons to string 1511 | * @param {Const} direction 1512 | * @returns {String} 1513 | */ 1514 | function directionStr(direction) { 1515 | if (direction == DIRECTION_DOWN) { 1516 | return 'down'; 1517 | } else if (direction == DIRECTION_UP) { 1518 | return 'up'; 1519 | } else if (direction == DIRECTION_LEFT) { 1520 | return 'left'; 1521 | } else if (direction == DIRECTION_RIGHT) { 1522 | return 'right'; 1523 | } 1524 | return ''; 1525 | } 1526 | 1527 | /** 1528 | * get a recognizer by name if it is bound to a manager 1529 | * @param {Recognizer|String} otherRecognizer 1530 | * @param {Recognizer} recognizer 1531 | * @returns {Recognizer} 1532 | */ 1533 | function getRecognizerByNameIfManager(otherRecognizer, recognizer) { 1534 | var manager = recognizer.manager; 1535 | if (manager) { 1536 | return manager.get(otherRecognizer); 1537 | } 1538 | return otherRecognizer; 1539 | } 1540 | 1541 | /** 1542 | * This recognizer is just used as a base for the simple attribute recognizers. 1543 | * @constructor 1544 | * @extends Recognizer 1545 | */ 1546 | function AttrRecognizer() { 1547 | Recognizer.apply(this, arguments); 1548 | } 1549 | 1550 | inherit(AttrRecognizer, Recognizer, { 1551 | /** 1552 | * @namespace 1553 | * @memberof AttrRecognizer 1554 | */ 1555 | defaults: { 1556 | /** 1557 | * @type {Number} 1558 | * @default 1 1559 | */ 1560 | pointers: 1 1561 | }, 1562 | 1563 | /** 1564 | * Used to check if it the recognizer receives valid input, like input.distance > 10. 1565 | * @memberof AttrRecognizer 1566 | * @param {Object} input 1567 | * @returns {Boolean} recognized 1568 | */ 1569 | attrTest: function(input) { 1570 | var optionPointers = this.options.pointers; 1571 | return optionPointers === 0 || input.pointers.length === optionPointers; 1572 | }, 1573 | 1574 | /** 1575 | * Process the input and return the state for the recognizer 1576 | * @memberof AttrRecognizer 1577 | * @param {Object} input 1578 | * @returns {*} State 1579 | */ 1580 | process: function(input) { 1581 | var state = this.state; 1582 | var eventType = input.eventType; 1583 | 1584 | var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); 1585 | var isValid = this.attrTest(input); 1586 | 1587 | // on cancel input and we've recognized before, return STATE_CANCELLED 1588 | if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { 1589 | return state | STATE_CANCELLED; 1590 | } else if (isRecognized || isValid) { 1591 | if (eventType & INPUT_END) { 1592 | return state | STATE_ENDED; 1593 | } else if (!(state & STATE_BEGAN)) { 1594 | return STATE_BEGAN; 1595 | } 1596 | return state | STATE_CHANGED; 1597 | } 1598 | return STATE_FAILED; 1599 | } 1600 | }); 1601 | 1602 | /** 1603 | * Pan 1604 | * Recognized when the pointer is down and moved in the allowed direction. 1605 | * @constructor 1606 | * @extends AttrRecognizer 1607 | */ 1608 | function PanRecognizer() { 1609 | AttrRecognizer.apply(this, arguments); 1610 | 1611 | this.pX = null; 1612 | this.pY = null; 1613 | } 1614 | 1615 | inherit(PanRecognizer, AttrRecognizer, { 1616 | /** 1617 | * @namespace 1618 | * @memberof PanRecognizer 1619 | */ 1620 | defaults: { 1621 | event: 'pan', 1622 | threshold: 10, 1623 | pointers: 1, 1624 | direction: DIRECTION_ALL 1625 | }, 1626 | 1627 | getTouchAction: function() { 1628 | var direction = this.options.direction; 1629 | var actions = []; 1630 | if (direction & DIRECTION_HORIZONTAL) { 1631 | actions.push(TOUCH_ACTION_PAN_Y); 1632 | } 1633 | if (direction & DIRECTION_VERTICAL) { 1634 | actions.push(TOUCH_ACTION_PAN_X); 1635 | } 1636 | return actions; 1637 | }, 1638 | 1639 | directionTest: function(input) { 1640 | var options = this.options; 1641 | var hasMoved = true; 1642 | var distance = input.distance; 1643 | var direction = input.direction; 1644 | var x = input.deltaX; 1645 | var y = input.deltaY; 1646 | 1647 | // lock to axis? 1648 | if (!(direction & options.direction)) { 1649 | if (options.direction & DIRECTION_HORIZONTAL) { 1650 | direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; 1651 | hasMoved = x != this.pX; 1652 | distance = Math.abs(input.deltaX); 1653 | } else { 1654 | direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN; 1655 | hasMoved = y != this.pY; 1656 | distance = Math.abs(input.deltaY); 1657 | } 1658 | } 1659 | input.direction = direction; 1660 | return hasMoved && distance > options.threshold && direction & options.direction; 1661 | }, 1662 | 1663 | attrTest: function(input) { 1664 | return AttrRecognizer.prototype.attrTest.call(this, input) && 1665 | (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); 1666 | }, 1667 | 1668 | emit: function(input) { 1669 | this.pX = input.deltaX; 1670 | this.pY = input.deltaY; 1671 | 1672 | var direction = directionStr(input.direction); 1673 | if (direction) { 1674 | this.manager.emit(this.options.event + direction, input); 1675 | } 1676 | 1677 | this._super.emit.call(this, input); 1678 | } 1679 | }); 1680 | 1681 | /** 1682 | * Pinch 1683 | * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). 1684 | * @constructor 1685 | * @extends AttrRecognizer 1686 | */ 1687 | function PinchRecognizer() { 1688 | AttrRecognizer.apply(this, arguments); 1689 | } 1690 | 1691 | inherit(PinchRecognizer, AttrRecognizer, { 1692 | /** 1693 | * @namespace 1694 | * @memberof PinchRecognizer 1695 | */ 1696 | defaults: { 1697 | event: 'pinch', 1698 | threshold: 0, 1699 | pointers: 2 1700 | }, 1701 | 1702 | getTouchAction: function() { 1703 | return [TOUCH_ACTION_NONE]; 1704 | }, 1705 | 1706 | attrTest: function(input) { 1707 | return this._super.attrTest.call(this, input) && 1708 | (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); 1709 | }, 1710 | 1711 | emit: function(input) { 1712 | this._super.emit.call(this, input); 1713 | if (input.scale !== 1) { 1714 | var inOut = input.scale < 1 ? 'in' : 'out'; 1715 | this.manager.emit(this.options.event + inOut, input); 1716 | } 1717 | } 1718 | }); 1719 | 1720 | /** 1721 | * Press 1722 | * Recognized when the pointer is down for x ms without any movement. 1723 | * @constructor 1724 | * @extends Recognizer 1725 | */ 1726 | function PressRecognizer() { 1727 | Recognizer.apply(this, arguments); 1728 | 1729 | this._timer = null; 1730 | this._input = null; 1731 | } 1732 | 1733 | inherit(PressRecognizer, Recognizer, { 1734 | /** 1735 | * @namespace 1736 | * @memberof PressRecognizer 1737 | */ 1738 | defaults: { 1739 | event: 'press', 1740 | pointers: 1, 1741 | time: 500, // minimal time of the pointer to be pressed 1742 | threshold: 5 // a minimal movement is ok, but keep it low 1743 | }, 1744 | 1745 | getTouchAction: function() { 1746 | return [TOUCH_ACTION_AUTO]; 1747 | }, 1748 | 1749 | process: function(input) { 1750 | var options = this.options; 1751 | var validPointers = input.pointers.length === options.pointers; 1752 | var validMovement = input.distance < options.threshold; 1753 | var validTime = input.deltaTime > options.time; 1754 | 1755 | this._input = input; 1756 | 1757 | // we only allow little movement 1758 | // and we've reached an end event, so a tap is possible 1759 | if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { 1760 | this.reset(); 1761 | } else if (input.eventType & INPUT_START) { 1762 | this.reset(); 1763 | this._timer = setTimeoutContext(function() { 1764 | this.state = STATE_RECOGNIZED; 1765 | this.tryEmit(); 1766 | }, options.time, this); 1767 | } else if (input.eventType & INPUT_END) { 1768 | return STATE_RECOGNIZED; 1769 | } 1770 | return STATE_FAILED; 1771 | }, 1772 | 1773 | reset: function() { 1774 | clearTimeout(this._timer); 1775 | }, 1776 | 1777 | emit: function(input) { 1778 | if (this.state !== STATE_RECOGNIZED) { 1779 | return; 1780 | } 1781 | 1782 | if (input && (input.eventType & INPUT_END)) { 1783 | this.manager.emit(this.options.event + 'up', input); 1784 | } else { 1785 | this._input.timeStamp = now(); 1786 | this.manager.emit(this.options.event, this._input); 1787 | } 1788 | } 1789 | }); 1790 | 1791 | /** 1792 | * Rotate 1793 | * Recognized when two or more pointer are moving in a circular motion. 1794 | * @constructor 1795 | * @extends AttrRecognizer 1796 | */ 1797 | function RotateRecognizer() { 1798 | AttrRecognizer.apply(this, arguments); 1799 | } 1800 | 1801 | inherit(RotateRecognizer, AttrRecognizer, { 1802 | /** 1803 | * @namespace 1804 | * @memberof RotateRecognizer 1805 | */ 1806 | defaults: { 1807 | event: 'rotate', 1808 | threshold: 0, 1809 | pointers: 2 1810 | }, 1811 | 1812 | getTouchAction: function() { 1813 | return [TOUCH_ACTION_NONE]; 1814 | }, 1815 | 1816 | attrTest: function(input) { 1817 | return this._super.attrTest.call(this, input) && 1818 | (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); 1819 | } 1820 | }); 1821 | 1822 | /** 1823 | * Swipe 1824 | * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. 1825 | * @constructor 1826 | * @extends AttrRecognizer 1827 | */ 1828 | function SwipeRecognizer() { 1829 | AttrRecognizer.apply(this, arguments); 1830 | } 1831 | 1832 | inherit(SwipeRecognizer, AttrRecognizer, { 1833 | /** 1834 | * @namespace 1835 | * @memberof SwipeRecognizer 1836 | */ 1837 | defaults: { 1838 | event: 'swipe', 1839 | threshold: 10, 1840 | velocity: 0.65, 1841 | direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, 1842 | pointers: 1 1843 | }, 1844 | 1845 | getTouchAction: function() { 1846 | return PanRecognizer.prototype.getTouchAction.call(this); 1847 | }, 1848 | 1849 | attrTest: function(input) { 1850 | var direction = this.options.direction; 1851 | var velocity; 1852 | 1853 | if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { 1854 | velocity = input.velocity; 1855 | } else if (direction & DIRECTION_HORIZONTAL) { 1856 | velocity = input.velocityX; 1857 | } else if (direction & DIRECTION_VERTICAL) { 1858 | velocity = input.velocityY; 1859 | } 1860 | 1861 | return this._super.attrTest.call(this, input) && 1862 | direction & input.direction && 1863 | input.distance > this.options.threshold && 1864 | abs(velocity) > this.options.velocity && input.eventType & INPUT_END; 1865 | }, 1866 | 1867 | emit: function(input) { 1868 | var direction = directionStr(input.direction); 1869 | if (direction) { 1870 | this.manager.emit(this.options.event + direction, input); 1871 | } 1872 | 1873 | this.manager.emit(this.options.event, input); 1874 | } 1875 | }); 1876 | 1877 | /** 1878 | * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur 1879 | * between the given interval and position. The delay option can be used to recognize multi-taps without firing 1880 | * a single tap. 1881 | * 1882 | * The eventData from the emitted event contains the property `tapCount`, which contains the amount of 1883 | * multi-taps being recognized. 1884 | * @constructor 1885 | * @extends Recognizer 1886 | */ 1887 | function TapRecognizer() { 1888 | Recognizer.apply(this, arguments); 1889 | 1890 | // previous time and center, 1891 | // used for tap counting 1892 | this.pTime = false; 1893 | this.pCenter = false; 1894 | 1895 | this._timer = null; 1896 | this._input = null; 1897 | this.count = 0; 1898 | } 1899 | 1900 | inherit(TapRecognizer, Recognizer, { 1901 | /** 1902 | * @namespace 1903 | * @memberof PinchRecognizer 1904 | */ 1905 | defaults: { 1906 | event: 'tap', 1907 | pointers: 1, 1908 | taps: 1, 1909 | interval: 300, // max time between the multi-tap taps 1910 | time: 250, // max time of the pointer to be down (like finger on the screen) 1911 | threshold: 2, // a minimal movement is ok, but keep it low 1912 | posThreshold: 10 // a multi-tap can be a bit off the initial position 1913 | }, 1914 | 1915 | getTouchAction: function() { 1916 | return [TOUCH_ACTION_MANIPULATION]; 1917 | }, 1918 | 1919 | process: function(input) { 1920 | var options = this.options; 1921 | 1922 | var validPointers = input.pointers.length === options.pointers; 1923 | var validMovement = input.distance < options.threshold; 1924 | var validTouchTime = input.deltaTime < options.time; 1925 | 1926 | this.reset(); 1927 | 1928 | if ((input.eventType & INPUT_START) && (this.count === 0)) { 1929 | return this.failTimeout(); 1930 | } 1931 | 1932 | // we only allow little movement 1933 | // and we've reached an end event, so a tap is possible 1934 | if (validMovement && validTouchTime && validPointers) { 1935 | if (input.eventType != INPUT_END) { 1936 | return this.failTimeout(); 1937 | } 1938 | 1939 | var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; 1940 | var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; 1941 | 1942 | this.pTime = input.timeStamp; 1943 | this.pCenter = input.center; 1944 | 1945 | if (!validMultiTap || !validInterval) { 1946 | this.count = 1; 1947 | } else { 1948 | this.count += 1; 1949 | } 1950 | 1951 | this._input = input; 1952 | 1953 | // if tap count matches we have recognized it, 1954 | // else it has began recognizing... 1955 | var tapCount = this.count % options.taps; 1956 | if (tapCount === 0) { 1957 | // no failing requirements, immediately trigger the tap event 1958 | // or wait as long as the multitap interval to trigger 1959 | if (!this.hasRequireFailures()) { 1960 | return STATE_RECOGNIZED; 1961 | } else { 1962 | this._timer = setTimeoutContext(function() { 1963 | this.state = STATE_RECOGNIZED; 1964 | this.tryEmit(); 1965 | }, options.interval, this); 1966 | return STATE_BEGAN; 1967 | } 1968 | } 1969 | } 1970 | return STATE_FAILED; 1971 | }, 1972 | 1973 | failTimeout: function() { 1974 | this._timer = setTimeoutContext(function() { 1975 | this.state = STATE_FAILED; 1976 | }, this.options.interval, this); 1977 | return STATE_FAILED; 1978 | }, 1979 | 1980 | reset: function() { 1981 | clearTimeout(this._timer); 1982 | }, 1983 | 1984 | emit: function() { 1985 | if (this.state == STATE_RECOGNIZED ) { 1986 | this._input.tapCount = this.count; 1987 | this.manager.emit(this.options.event, this._input); 1988 | } 1989 | } 1990 | }); 1991 | 1992 | /** 1993 | * Simple way to create an manager with a default set of recognizers. 1994 | * @param {HTMLElement} element 1995 | * @param {Object} [options] 1996 | * @constructor 1997 | */ 1998 | function Hammer(element, options) { 1999 | options = options || {}; 2000 | options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); 2001 | return new Manager(element, options); 2002 | } 2003 | 2004 | /** 2005 | * @const {string} 2006 | */ 2007 | Hammer.VERSION = '2.0.4'; 2008 | 2009 | /** 2010 | * default settings 2011 | * @namespace 2012 | */ 2013 | Hammer.defaults = { 2014 | /** 2015 | * set if DOM events are being triggered. 2016 | * But this is slower and unused by simple implementations, so disabled by default. 2017 | * @type {Boolean} 2018 | * @default false 2019 | */ 2020 | domEvents: false, 2021 | 2022 | /** 2023 | * The value for the touchAction property/fallback. 2024 | * When set to `compute` it will magically set the correct value based on the added recognizers. 2025 | * @type {String} 2026 | * @default compute 2027 | */ 2028 | touchAction: TOUCH_ACTION_COMPUTE, 2029 | 2030 | /** 2031 | * @type {Boolean} 2032 | * @default true 2033 | */ 2034 | enable: true, 2035 | 2036 | /** 2037 | * EXPERIMENTAL FEATURE -- can be removed/changed 2038 | * Change the parent input target element. 2039 | * If Null, then it is being set the to main element. 2040 | * @type {Null|EventTarget} 2041 | * @default null 2042 | */ 2043 | inputTarget: null, 2044 | 2045 | /** 2046 | * force an input class 2047 | * @type {Null|Function} 2048 | * @default null 2049 | */ 2050 | inputClass: null, 2051 | 2052 | /** 2053 | * Default recognizer setup when calling `Hammer()` 2054 | * When creating a new Manager these will be skipped. 2055 | * @type {Array} 2056 | */ 2057 | preset: [ 2058 | // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] 2059 | [RotateRecognizer, { enable: false }], 2060 | [PinchRecognizer, { enable: false }, ['rotate']], 2061 | [SwipeRecognizer,{ direction: DIRECTION_HORIZONTAL }], 2062 | [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], 2063 | [TapRecognizer], 2064 | [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], 2065 | [PressRecognizer] 2066 | ], 2067 | 2068 | /** 2069 | * Some CSS properties can be used to improve the working of Hammer. 2070 | * Add them to this method and they will be set when creating a new Manager. 2071 | * @namespace 2072 | */ 2073 | cssProps: { 2074 | /** 2075 | * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. 2076 | * @type {String} 2077 | * @default 'none' 2078 | */ 2079 | userSelect: 'none', 2080 | 2081 | /** 2082 | * Disable the Windows Phone grippers when pressing an element. 2083 | * @type {String} 2084 | * @default 'none' 2085 | */ 2086 | touchSelect: 'none', 2087 | 2088 | /** 2089 | * Disables the default callout shown when you touch and hold a touch target. 2090 | * On iOS, when you touch and hold a touch target such as a link, Safari displays 2091 | * a callout containing information about the link. This property allows you to disable that callout. 2092 | * @type {String} 2093 | * @default 'none' 2094 | */ 2095 | touchCallout: 'none', 2096 | 2097 | /** 2098 | * Specifies whether zooming is enabled. Used by IE10> 2099 | * @type {String} 2100 | * @default 'none' 2101 | */ 2102 | contentZooming: 'none', 2103 | 2104 | /** 2105 | * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. 2106 | * @type {String} 2107 | * @default 'none' 2108 | */ 2109 | userDrag: 'none', 2110 | 2111 | /** 2112 | * Overrides the highlight color shown when the user taps a link or a JavaScript 2113 | * clickable element in iOS. This property obeys the alpha value, if specified. 2114 | * @type {String} 2115 | * @default 'rgba(0,0,0,0)' 2116 | */ 2117 | tapHighlightColor: 'rgba(0,0,0,0)' 2118 | } 2119 | }; 2120 | 2121 | var STOP = 1; 2122 | var FORCED_STOP = 2; 2123 | 2124 | /** 2125 | * Manager 2126 | * @param {HTMLElement} element 2127 | * @param {Object} [options] 2128 | * @constructor 2129 | */ 2130 | function Manager(element, options) { 2131 | options = options || {}; 2132 | 2133 | this.options = merge(options, Hammer.defaults); 2134 | this.options.inputTarget = this.options.inputTarget || element; 2135 | 2136 | this.handlers = {}; 2137 | this.session = {}; 2138 | this.recognizers = []; 2139 | 2140 | this.element = element; 2141 | this.input = createInputInstance(this); 2142 | this.touchAction = new TouchAction(this, this.options.touchAction); 2143 | 2144 | toggleCssProps(this, true); 2145 | 2146 | each(options.recognizers, function(item) { 2147 | var recognizer = this.add(new (item[0])(item[1])); 2148 | item[2] && recognizer.recognizeWith(item[2]); 2149 | item[3] && recognizer.requireFailure(item[3]); 2150 | }, this); 2151 | } 2152 | 2153 | Manager.prototype = { 2154 | /** 2155 | * set options 2156 | * @param {Object} options 2157 | * @returns {Manager} 2158 | */ 2159 | set: function(options) { 2160 | extend(this.options, options); 2161 | 2162 | // Options that need a little more setup 2163 | if (options.touchAction) { 2164 | this.touchAction.update(); 2165 | } 2166 | if (options.inputTarget) { 2167 | // Clean up existing event listeners and reinitialize 2168 | this.input.destroy(); 2169 | this.input.target = options.inputTarget; 2170 | this.input.init(); 2171 | } 2172 | return this; 2173 | }, 2174 | 2175 | /** 2176 | * stop recognizing for this session. 2177 | * This session will be discarded, when a new [input]start event is fired. 2178 | * When forced, the recognizer cycle is stopped immediately. 2179 | * @param {Boolean} [force] 2180 | */ 2181 | stop: function(force) { 2182 | this.session.stopped = force ? FORCED_STOP : STOP; 2183 | }, 2184 | 2185 | /** 2186 | * run the recognizers! 2187 | * called by the inputHandler function on every movement of the pointers (touches) 2188 | * it walks through all the recognizers and tries to detect the gesture that is being made 2189 | * @param {Object} inputData 2190 | */ 2191 | recognize: function(inputData) { 2192 | var session = this.session; 2193 | if (session.stopped) { 2194 | return; 2195 | } 2196 | 2197 | // run the touch-action polyfill 2198 | this.touchAction.preventDefaults(inputData); 2199 | 2200 | var recognizer; 2201 | var recognizers = this.recognizers; 2202 | 2203 | // this holds the recognizer that is being recognized. 2204 | // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED 2205 | // if no recognizer is detecting a thing, it is set to `null` 2206 | var curRecognizer = session.curRecognizer; 2207 | 2208 | // reset when the last recognizer is recognized 2209 | // or when we're in a new session 2210 | if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { 2211 | curRecognizer = session.curRecognizer = null; 2212 | } 2213 | 2214 | var i = 0; 2215 | while (i < recognizers.length) { 2216 | recognizer = recognizers[i]; 2217 | 2218 | // find out if we are allowed try to recognize the input for this one. 2219 | // 1. allow if the session is NOT forced stopped (see the .stop() method) 2220 | // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one 2221 | // that is being recognized. 2222 | // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. 2223 | // this can be setup with the `recognizeWith()` method on the recognizer. 2224 | if (session.stopped !== FORCED_STOP && ( // 1 2225 | !curRecognizer || recognizer == curRecognizer || // 2 2226 | recognizer.canRecognizeWith(curRecognizer))) { // 3 2227 | recognizer.recognize(inputData); 2228 | } else { 2229 | recognizer.reset(); 2230 | } 2231 | 2232 | // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the 2233 | // current active recognizer. but only if we don't already have an active recognizer 2234 | if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { 2235 | curRecognizer = session.curRecognizer = recognizer; 2236 | } 2237 | i++; 2238 | } 2239 | }, 2240 | 2241 | /** 2242 | * get a recognizer by its event name. 2243 | * @param {Recognizer|String} recognizer 2244 | * @returns {Recognizer|Null} 2245 | */ 2246 | get: function(recognizer) { 2247 | if (recognizer instanceof Recognizer) { 2248 | return recognizer; 2249 | } 2250 | 2251 | var recognizers = this.recognizers; 2252 | for (var i = 0; i < recognizers.length; i++) { 2253 | if (recognizers[i].options.event == recognizer) { 2254 | return recognizers[i]; 2255 | } 2256 | } 2257 | return null; 2258 | }, 2259 | 2260 | /** 2261 | * add a recognizer to the manager 2262 | * existing recognizers with the same event name will be removed 2263 | * @param {Recognizer} recognizer 2264 | * @returns {Recognizer|Manager} 2265 | */ 2266 | add: function(recognizer) { 2267 | if (invokeArrayArg(recognizer, 'add', this)) { 2268 | return this; 2269 | } 2270 | 2271 | // remove existing 2272 | var existing = this.get(recognizer.options.event); 2273 | if (existing) { 2274 | this.remove(existing); 2275 | } 2276 | 2277 | this.recognizers.push(recognizer); 2278 | recognizer.manager = this; 2279 | 2280 | this.touchAction.update(); 2281 | return recognizer; 2282 | }, 2283 | 2284 | /** 2285 | * remove a recognizer by name or instance 2286 | * @param {Recognizer|String} recognizer 2287 | * @returns {Manager} 2288 | */ 2289 | remove: function(recognizer) { 2290 | if (invokeArrayArg(recognizer, 'remove', this)) { 2291 | return this; 2292 | } 2293 | 2294 | var recognizers = this.recognizers; 2295 | recognizer = this.get(recognizer); 2296 | recognizers.splice(inArray(recognizers, recognizer), 1); 2297 | 2298 | this.touchAction.update(); 2299 | return this; 2300 | }, 2301 | 2302 | /** 2303 | * bind event 2304 | * @param {String} events 2305 | * @param {Function} handler 2306 | * @returns {EventEmitter} this 2307 | */ 2308 | on: function(events, handler) { 2309 | var handlers = this.handlers; 2310 | each(splitStr(events), function(event) { 2311 | handlers[event] = handlers[event] || []; 2312 | handlers[event].push(handler); 2313 | }); 2314 | return this; 2315 | }, 2316 | 2317 | /** 2318 | * unbind event, leave emit blank to remove all handlers 2319 | * @param {String} events 2320 | * @param {Function} [handler] 2321 | * @returns {EventEmitter} this 2322 | */ 2323 | off: function(events, handler) { 2324 | var handlers = this.handlers; 2325 | each(splitStr(events), function(event) { 2326 | if (!handler) { 2327 | delete handlers[event]; 2328 | } else { 2329 | handlers[event].splice(inArray(handlers[event], handler), 1); 2330 | } 2331 | }); 2332 | return this; 2333 | }, 2334 | 2335 | /** 2336 | * emit event to the listeners 2337 | * @param {String} event 2338 | * @param {Object} data 2339 | */ 2340 | emit: function(event, data) { 2341 | // we also want to trigger dom events 2342 | if (this.options.domEvents) { 2343 | triggerDomEvent(event, data); 2344 | } 2345 | 2346 | // no handlers, so skip it all 2347 | var handlers = this.handlers[event] && this.handlers[event].slice(); 2348 | if (!handlers || !handlers.length) { 2349 | return; 2350 | } 2351 | 2352 | data.type = event; 2353 | data.preventDefault = function() { 2354 | data.srcEvent.preventDefault(); 2355 | }; 2356 | 2357 | var i = 0; 2358 | while (i < handlers.length) { 2359 | handlers[i](data); 2360 | i++; 2361 | } 2362 | }, 2363 | 2364 | /** 2365 | * destroy the manager and unbinds all events 2366 | * it doesn't unbind dom events, that is the user own responsibility 2367 | */ 2368 | destroy: function() { 2369 | this.element && toggleCssProps(this, false); 2370 | 2371 | this.handlers = {}; 2372 | this.session = {}; 2373 | this.input.destroy(); 2374 | this.element = null; 2375 | } 2376 | }; 2377 | 2378 | /** 2379 | * add/remove the css properties as defined in manager.options.cssProps 2380 | * @param {Manager} manager 2381 | * @param {Boolean} add 2382 | */ 2383 | function toggleCssProps(manager, add) { 2384 | var element = manager.element; 2385 | each(manager.options.cssProps, function(value, name) { 2386 | element.style[prefixed(element.style, name)] = add ? value : ''; 2387 | }); 2388 | } 2389 | 2390 | /** 2391 | * trigger dom event 2392 | * @param {String} event 2393 | * @param {Object} data 2394 | */ 2395 | function triggerDomEvent(event, data) { 2396 | var gestureEvent = document.createEvent('Event'); 2397 | gestureEvent.initEvent(event, true, true); 2398 | gestureEvent.gesture = data; 2399 | data.target.dispatchEvent(gestureEvent); 2400 | } 2401 | 2402 | extend(Hammer, { 2403 | INPUT_START: INPUT_START, 2404 | INPUT_MOVE: INPUT_MOVE, 2405 | INPUT_END: INPUT_END, 2406 | INPUT_CANCEL: INPUT_CANCEL, 2407 | 2408 | STATE_POSSIBLE: STATE_POSSIBLE, 2409 | STATE_BEGAN: STATE_BEGAN, 2410 | STATE_CHANGED: STATE_CHANGED, 2411 | STATE_ENDED: STATE_ENDED, 2412 | STATE_RECOGNIZED: STATE_RECOGNIZED, 2413 | STATE_CANCELLED: STATE_CANCELLED, 2414 | STATE_FAILED: STATE_FAILED, 2415 | 2416 | DIRECTION_NONE: DIRECTION_NONE, 2417 | DIRECTION_LEFT: DIRECTION_LEFT, 2418 | DIRECTION_RIGHT: DIRECTION_RIGHT, 2419 | DIRECTION_UP: DIRECTION_UP, 2420 | DIRECTION_DOWN: DIRECTION_DOWN, 2421 | DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, 2422 | DIRECTION_VERTICAL: DIRECTION_VERTICAL, 2423 | DIRECTION_ALL: DIRECTION_ALL, 2424 | 2425 | Manager: Manager, 2426 | Input: Input, 2427 | TouchAction: TouchAction, 2428 | 2429 | TouchInput: TouchInput, 2430 | MouseInput: MouseInput, 2431 | PointerEventInput: PointerEventInput, 2432 | TouchMouseInput: TouchMouseInput, 2433 | SingleTouchInput: SingleTouchInput, 2434 | 2435 | Recognizer: Recognizer, 2436 | AttrRecognizer: AttrRecognizer, 2437 | Tap: TapRecognizer, 2438 | Pan: PanRecognizer, 2439 | Swipe: SwipeRecognizer, 2440 | Pinch: PinchRecognizer, 2441 | Rotate: RotateRecognizer, 2442 | Press: PressRecognizer, 2443 | 2444 | on: addEventListeners, 2445 | off: removeEventListeners, 2446 | each: each, 2447 | merge: merge, 2448 | extend: extend, 2449 | inherit: inherit, 2450 | bindFn: bindFn, 2451 | prefixed: prefixed 2452 | }); 2453 | 2454 | if (typeof define == TYPE_FUNCTION && define.amd) { 2455 | define(function() { 2456 | return Hammer; 2457 | }); 2458 | } else if (typeof module != 'undefined' && module.exports) { 2459 | module.exports = Hammer; 2460 | } else { 2461 | window[exportName] = Hammer; 2462 | } 2463 | 2464 | })(window, document, 'Hammer'); 2465 | 2466 | },{}],2:[function(require,module,exports){ 2467 | /** 2468 | * @fileoverview Hammer driver for Cycle.js 2469 | * @author Frederik Krautwald 2470 | * @copyright 2015 Cyclic Materials. All rights reserved. 2471 | */ 2472 | 2473 | 'use strict'; 2474 | 2475 | Object.defineProperty(exports, '__esModule', { 2476 | value: true 2477 | }); 2478 | 2479 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 2480 | 2481 | var _makeHammerDriver = require('./makeHammerDriver'); 2482 | 2483 | var _makeHammerDriver2 = _interopRequireDefault(_makeHammerDriver); 2484 | 2485 | var CycleHammer = { 2486 | /** 2487 | * A factory for the Hammer driver function. Takes a DOM driver function that 2488 | * adheres to the interface established by the Cycle.js DOM driver. 2489 | * The output (source) of this driver mimics the DOM driver as a collection 2490 | * of Observables queried with: `DOM.select(selector).events(eventType)`. 2491 | * In addition, when listening for hammer.js `eventTypes`, the `events()` 2492 | * function allows you to specify an optional callback: 2493 | * `DOM.select(selector).events(eventType, callback)`. 2494 | * The `callback` takes two arguments: 2495 | * 2496 | * - **manager** `{Object}` The `Hammer.Manager` instance for the element. 2497 | * - **Hammer** `{Object}` The Hammer API. 2498 | * 2499 | * @param {Function} domDriverFunc the DOM driver function. 2500 | * @return {Function} the Hammer driver function. The function expects an 2501 | * Observable of VTree as input, and outputs the source object for this 2502 | * driver, containing functions `select()` and `dispose()` that can be used 2503 | * for debugging and testing. 2504 | * @function makeDOMDriver 2505 | */ 2506 | makeHammerDriver: _makeHammerDriver2['default'] 2507 | }; 2508 | 2509 | exports['default'] = CycleHammer; 2510 | module.exports = exports['default']; 2511 | 2512 | },{"./makeHammerDriver":5}],3:[function(require,module,exports){ 2513 | (function (global){ 2514 | /** 2515 | * @fileoverview Observable from Hammer Event 2516 | * @author Frederik Krautwald 2517 | * @copyright 2015 Frederik Krautwald. All rights reserved. 2518 | */ 2519 | 2520 | 'use strict'; 2521 | 2522 | Object.defineProperty(exports, '__esModule', { 2523 | value: true 2524 | }); 2525 | 2526 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 2527 | 2528 | var _rx = (typeof window !== "undefined" ? window['Rx'] : typeof global !== "undefined" ? global['Rx'] : null); 2529 | 2530 | var _hammerjs = require('hammerjs'); 2531 | 2532 | var _hammerjs2 = _interopRequireDefault(_hammerjs); 2533 | 2534 | var hammers = {}; 2535 | 2536 | function createListener(_ref) { 2537 | var element = _ref.element; 2538 | var eventType = _ref.eventType; 2539 | var handler = _ref.handler; 2540 | var k = _ref.k; 2541 | var _ref$callback = _ref.callback; 2542 | var callback = _ref$callback === undefined ? function () {} : _ref$callback; 2543 | 2544 | if (typeof hammers[k] !== 'object') { 2545 | hammers[k] = new _hammerjs2['default'].Manager(element); 2546 | } 2547 | var hammer = hammers[k]; 2548 | callback(hammer, _hammerjs2['default']); 2549 | hammer.on(eventType, handler); 2550 | } 2551 | 2552 | function createEventListener(_ref2) { 2553 | var element = _ref2.element; 2554 | var eventType = _ref2.eventType; 2555 | var handler = _ref2.handler; 2556 | var k = _ref2.k; 2557 | var callback = _ref2.callback; 2558 | 2559 | if (Array.isArray(element)) { 2560 | for (var i = 0, len = element.length; i < len; i++) { 2561 | createEventListener({ 2562 | element: element[i], 2563 | eventType: eventType, 2564 | handler: handler, 2565 | k: k, 2566 | callback: callback 2567 | }); 2568 | } 2569 | } else if (element) { 2570 | createListener({ element: element, eventType: eventType, handler: handler, k: k, callback: callback }); 2571 | } 2572 | } 2573 | 2574 | var elementEvents = {}; 2575 | 2576 | function fromEvent(_ref3) { 2577 | var element = _ref3.element; 2578 | var eventType = _ref3.eventType; 2579 | var k = _ref3.k; 2580 | var callback = _ref3.callback; 2581 | 2582 | if (typeof elementEvents[k] !== 'object') { 2583 | elementEvents[k] = {}; 2584 | } 2585 | var elementEvent = elementEvents[k]; 2586 | var event$ = undefined; 2587 | if (elementEvent[eventType]) { 2588 | event$ = elementEvent[eventType]; 2589 | } else { 2590 | elementEvent[eventType] = new _rx.Subject(); 2591 | event$ = elementEvent[eventType]; 2592 | createEventListener({ 2593 | element: element, 2594 | eventType: eventType, 2595 | handler: function handler(value) { 2596 | //console.log(`handler`, value.type) 2597 | event$.onNext(value); 2598 | }, 2599 | k: k, 2600 | callback: callback 2601 | }); 2602 | } 2603 | return event$; 2604 | } 2605 | 2606 | exports['default'] = fromEvent; 2607 | module.exports = exports['default']; 2608 | 2609 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 2610 | },{"hammerjs":1}],4:[function(require,module,exports){ 2611 | (function (global){ 2612 | /** 2613 | * @fileoverview Hammer driver 2614 | * @author Frederik Krautwald 2615 | * @copyright 2015 Cyclic Materials. All rights reserved. 2616 | */ 2617 | 2618 | 'use strict'; 2619 | 2620 | Object.defineProperty(exports, '__esModule', { 2621 | value: true 2622 | }); 2623 | 2624 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 2625 | 2626 | var _rx = (typeof window !== "undefined" ? window['Rx'] : typeof global !== "undefined" ? global['Rx'] : null); 2627 | 2628 | var _fromEvent = require('./fromEvent'); 2629 | 2630 | var _fromEvent2 = _interopRequireDefault(_fromEvent); 2631 | 2632 | var _standardEventTypes = require('./standardEventTypes'); 2633 | 2634 | var _standardEventTypes2 = _interopRequireDefault(_standardEventTypes); 2635 | 2636 | function fromHammerEvent(eventType, api, callback) { 2637 | return api.observable.flatMapLatest(function (element) { 2638 | if (!element) { 2639 | return _rx.Observable.empty(); 2640 | } 2641 | var k = api.namespace.join(''); 2642 | return (0, _fromEvent2['default'])({ element: element, eventType: eventType, k: k, callback: callback }); 2643 | }).share(); 2644 | } 2645 | 2646 | function makeEventsSelector(api, originalEvents) { 2647 | return function events(eventType) { 2648 | for (var _len = arguments.length, rest = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 2649 | rest[_key - 1] = arguments[_key]; 2650 | } 2651 | 2652 | return _standardEventTypes2['default'][eventType] ? originalEvents.apply(undefined, [eventType].concat(rest)) : fromHammerEvent.apply(undefined, [eventType, api].concat(rest)); 2653 | }; 2654 | } 2655 | 2656 | function override(obj, methodName, callback) { 2657 | obj[methodName] = callback(obj[methodName]); 2658 | } 2659 | 2660 | function selectCallback(originalSelect) { 2661 | return function select() { 2662 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 2663 | args[_key2] = arguments[_key2]; 2664 | } 2665 | 2666 | var api = originalSelect.apply(this, args); 2667 | override(api, 'select', selectCallback); 2668 | var originalEvents = api.events; 2669 | api.events = makeEventsSelector(api, originalEvents); 2670 | return api; 2671 | }; 2672 | } 2673 | 2674 | function hammerDriver(domDriverFunc, vTree$) { 2675 | if (typeof domDriverFunc !== 'function') { 2676 | throw new Error('First argument given to hammerDriver() must be a DOM driver function.'); 2677 | } 2678 | if (!vTree$ || typeof vTree$.subscribe !== 'function') { 2679 | throw new Error('The Hammer Driver expects a stream of virtual DOM elements as input.'); 2680 | } 2681 | 2682 | var domDriver = domDriverFunc(vTree$); 2683 | override(domDriver, 'select', selectCallback); 2684 | 2685 | return domDriver; 2686 | } 2687 | 2688 | exports['default'] = hammerDriver; 2689 | module.exports = exports['default']; 2690 | 2691 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 2692 | },{"./fromEvent":3,"./standardEventTypes":6}],5:[function(require,module,exports){ 2693 | /** 2694 | * @fileoverview Hammer driver factory 2695 | * @author Frederik Krautwald 2696 | * @copyright 2015 Cyclic Materials. All rights reserved. 2697 | */ 2698 | 2699 | 'use strict'; 2700 | 2701 | Object.defineProperty(exports, '__esModule', { 2702 | value: true 2703 | }); 2704 | 2705 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 2706 | 2707 | var _hammerDriver = require('./hammerDriver'); 2708 | 2709 | var _hammerDriver2 = _interopRequireDefault(_hammerDriver); 2710 | 2711 | function makeHammerDriver(domDriverFunc) { 2712 | if (typeof domDriverFunc !== 'function') { 2713 | throw new Error('First argument given to makeHammerDriver() ' + 'must be a DOM driver function.'); 2714 | } 2715 | return _hammerDriver2['default'].bind({}, domDriverFunc); 2716 | } 2717 | 2718 | exports['default'] = makeHammerDriver; 2719 | module.exports = exports['default']; 2720 | 2721 | },{"./hammerDriver":4}],6:[function(require,module,exports){ 2722 | /** 2723 | * @fileoverview Standard Event Types 2724 | * @author Frederik Krautwald 2725 | * @copyright 2015 Cyclic Materials. All rights reserved. 2726 | */ 2727 | 2728 | // https://developer.mozilla.org/en-US/docs/Web/Events 2729 | "use strict"; 2730 | 2731 | Object.defineProperty(exports, "__esModule", { 2732 | value: true 2733 | }); 2734 | var standardEventTypes = { 2735 | abort: true, 2736 | afterprint: true, 2737 | animationend: true, 2738 | animationiteration: true, 2739 | animationstart: true, 2740 | audioprocess: true, 2741 | audioend: true, 2742 | audiostart: true, 2743 | beforeprint: true, 2744 | beforeunload: true, 2745 | beginEvent: true, 2746 | blocked: true, 2747 | blur: true, 2748 | cached: true, 2749 | canplay: true, 2750 | canplaythrough: true, 2751 | change: true, 2752 | chargingchange: true, 2753 | chargingtimechange: true, 2754 | checking: true, 2755 | click: true, 2756 | close: true, 2757 | complete: true, 2758 | compositionend: true, 2759 | compositionstart: true, 2760 | compositionupdate: true, 2761 | contextmenu: true, 2762 | copy: true, 2763 | cut: true, 2764 | dblclick: true, 2765 | devicelight: true, 2766 | devicemotion: true, 2767 | deviceorientation: true, 2768 | deviceproximity: true, 2769 | dischargingtimechange: true, 2770 | DOMActivate: true, 2771 | DOMAttributeNameChanged: true, 2772 | DOMAttrModified: true, 2773 | DOMCharacterDataModified: true, 2774 | DOMContentLoaded: true, 2775 | DOMElementNameChanged: true, 2776 | DOMFocusIn: true, 2777 | DOMFocusOut: true, 2778 | DOMNodeInserted: true, 2779 | DOMNodeInsertedIntoDocument: true, 2780 | DOMNodeRemoved: true, 2781 | DOMNodeRemovedFromDocument: true, 2782 | DOMSubtreeModified: true, 2783 | downloading: true, 2784 | drag: true, 2785 | dragend: true, 2786 | dragenter: true, 2787 | dragleave: true, 2788 | dragover: true, 2789 | dragstart: true, 2790 | drop: true, 2791 | durationchange: true, 2792 | emptied: true, 2793 | end: true, 2794 | ended: true, 2795 | endEvent: true, 2796 | focus: true, 2797 | focusin: true, 2798 | focusout: true, 2799 | fullscreenchange: true, 2800 | fullscreenerror: true, 2801 | gamepadconnected: true, 2802 | gamepaddisconnected: true, 2803 | gotpointercapture: true, 2804 | hashchange: true, 2805 | lostpointercapture: true, 2806 | input: true, 2807 | invalid: true, 2808 | keydown: true, 2809 | keypress: true, 2810 | keyup: true, 2811 | languagechange: true, 2812 | levelchange: true, 2813 | load: true, 2814 | loadeddata: true, 2815 | loadedmetadata: true, 2816 | loadend: true, 2817 | loadstart: true, 2818 | message: true, 2819 | mousedown: true, 2820 | mouseenter: true, 2821 | mouseleave: true, 2822 | mousemove: true, 2823 | mouseout: true, 2824 | mouseover: true, 2825 | mouseup: true, 2826 | nomatch: true, 2827 | notificationclick: true, 2828 | noupdate: true, 2829 | obsolete: true, 2830 | offline: true, 2831 | online: true, 2832 | open: true, 2833 | orientationchange: true, 2834 | pagehide: true, 2835 | pageshow: true, 2836 | paste: true, 2837 | pause: true, 2838 | pointercancel: true, 2839 | pointerdown: true, 2840 | pointerenter: true, 2841 | pointerleave: true, 2842 | pointerlockchange: true, 2843 | pointerlockerror: true, 2844 | pointermove: true, 2845 | pointerout: true, 2846 | pointerover: true, 2847 | pointerup: true, 2848 | play: true, 2849 | playing: true, 2850 | popstate: true, 2851 | progress: true, 2852 | push: true, 2853 | pushsubscriptionchange: true, 2854 | ratechange: true, 2855 | readystatechange: true, 2856 | repeatEvent: true, 2857 | reset: true, 2858 | resize: true, 2859 | result: true, 2860 | scroll: true, 2861 | seeked: true, 2862 | seeking: true, 2863 | select: true, 2864 | selectstart: true, 2865 | selectionchange: true, 2866 | show: true, 2867 | soundend: true, 2868 | soundstart: true, 2869 | speechend: true, 2870 | speechstart: true, 2871 | stalled: true, 2872 | start: true, 2873 | storage: true, 2874 | submit: true, 2875 | success: true, 2876 | suspend: true, 2877 | SVGAbort: true, 2878 | SVGError: true, 2879 | SVGLoad: true, 2880 | SVGResize: true, 2881 | SVGScroll: true, 2882 | SVGUnload: true, 2883 | SVGZoom: true, 2884 | timeout: true, 2885 | timeupdate: true, 2886 | touchcancel: true, 2887 | touchend: true, 2888 | touchenter: true, 2889 | touchleave: true, 2890 | touchmove: true, 2891 | touchstart: true, 2892 | transitionend: true, 2893 | unload: true, 2894 | updateready: true, 2895 | upgradeneeded: true, 2896 | userproximity: true, 2897 | versionchange: true, 2898 | visibilitychange: true, 2899 | volumechange: true, 2900 | waiting: true, 2901 | wheel: true 2902 | }; 2903 | 2904 | //const hammerEventTable = { 2905 | // pan: true, 2906 | // panstart: true, 2907 | // panmove: true, 2908 | // panend: true, 2909 | // pancancel: true, 2910 | // panleft: true, 2911 | // panright: true, 2912 | // panup: true, 2913 | // pandown: true, 2914 | // pinch: true, 2915 | // pinchstart: true, 2916 | // pinchmove: true, 2917 | // pinchend: true, 2918 | // pinchcancel: true, 2919 | // pinchin: true, 2920 | // pinchout: true, 2921 | // press: true, 2922 | // pressup: true, 2923 | // rotate: true, 2924 | // rotatestart: true, 2925 | // rotatemove: true, 2926 | // rotateend: true, 2927 | // rotatecancel: true, 2928 | // swipe: true, 2929 | // swipeleft: true, 2930 | // swiperight: true, 2931 | // swipeup: true, 2932 | // swipedown: true, 2933 | // tap: true, 2934 | //} 2935 | 2936 | exports["default"] = standardEventTypes; 2937 | module.exports = exports["default"]; 2938 | 2939 | },{}]},{},[2])(2) 2940 | }); -------------------------------------------------------------------------------- /dist/cycle-hammer.min.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.CycleHammer=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o-1}function splitStr(str){return str.trim().split(/\s+/g)}function inArray(src,find,findByKey){if(src.indexOf&&!findByKey){return src.indexOf(find)}else{var i=0;while(ib[key]})}}return results}function prefixed(obj,property){var prefix,prop;var camelProp=property[0].toUpperCase()+property.slice(1);var i=0;while(i1&&!session.firstMultiple){session.firstMultiple=simpleCloneInputData(input)}else if(pointersLength===1){session.firstMultiple=false}var firstInput=session.firstInput;var firstMultiple=session.firstMultiple;var offsetCenter=firstMultiple?firstMultiple.center:firstInput.center;var center=input.center=getCenter(pointers);input.timeStamp=now();input.deltaTime=input.timeStamp-firstInput.timeStamp;input.angle=getAngle(offsetCenter,center);input.distance=getDistance(offsetCenter,center);computeDeltaXY(session,input);input.offsetDirection=getDirection(input.deltaX,input.deltaY);input.scale=firstMultiple?getScale(firstMultiple.pointers,pointers):1;input.rotation=firstMultiple?getRotation(firstMultiple.pointers,pointers):0;computeIntervalInputData(session,input);var target=manager.element;if(hasParent(input.srcEvent.target,target)){target=input.srcEvent.target}input.target=target}function computeDeltaXY(session,input){var center=input.center;var offset=session.offsetDelta||{};var prevDelta=session.prevDelta||{};var prevInput=session.prevInput||{};if(input.eventType===INPUT_START||prevInput.eventType===INPUT_END){prevDelta=session.prevDelta={x:prevInput.deltaX||0,y:prevInput.deltaY||0};offset=session.offsetDelta={x:center.x,y:center.y}}input.deltaX=prevDelta.x+(center.x-offset.x);input.deltaY=prevDelta.y+(center.y-offset.y)}function computeIntervalInputData(session,input){var last=session.lastInterval||input,deltaTime=input.timeStamp-last.timeStamp,velocity,velocityX,velocityY,direction;if(input.eventType!=INPUT_CANCEL&&(deltaTime>COMPUTE_INTERVAL||last.velocity===undefined)){var deltaX=last.deltaX-input.deltaX;var deltaY=last.deltaY-input.deltaY;var v=getVelocity(deltaTime,deltaX,deltaY);velocityX=v.x;velocityY=v.y;velocity=abs(v.x)>abs(v.y)?v.x:v.y;direction=getDirection(deltaX,deltaY);session.lastInterval=input}else{velocity=last.velocity;velocityX=last.velocityX;velocityY=last.velocityY;direction=last.direction}input.velocity=velocity;input.velocityX=velocityX;input.velocityY=velocityY;input.direction=direction}function simpleCloneInputData(input){var pointers=[];var i=0;while(i=abs(y)){return x>0?DIRECTION_LEFT:DIRECTION_RIGHT}return y>0?DIRECTION_UP:DIRECTION_DOWN}function getDistance(p1,p2,props){if(!props){props=PROPS_XY}var x=p2[props[0]]-p1[props[0]],y=p2[props[1]]-p1[props[1]];return Math.sqrt(x*x+y*y)}function getAngle(p1,p2,props){if(!props){props=PROPS_XY}var x=p2[props[0]]-p1[props[0]],y=p2[props[1]]-p1[props[1]];return Math.atan2(y,x)*180/Math.PI}function getRotation(start,end){return getAngle(end[1],end[0],PROPS_CLIENT_XY)-getAngle(start[1],start[0],PROPS_CLIENT_XY)}function getScale(start,end){return getDistance(end[0],end[1],PROPS_CLIENT_XY)/getDistance(start[0],start[1],PROPS_CLIENT_XY)}var MOUSE_INPUT_MAP={mousedown:INPUT_START,mousemove:INPUT_MOVE,mouseup:INPUT_END};var MOUSE_ELEMENT_EVENTS="mousedown";var MOUSE_WINDOW_EVENTS="mousemove mouseup";function MouseInput(){this.evEl=MOUSE_ELEMENT_EVENTS;this.evWin=MOUSE_WINDOW_EVENTS;this.allow=true;this.pressed=false;Input.apply(this,arguments)}inherit(MouseInput,Input,{handler:function MEhandler(ev){var eventType=MOUSE_INPUT_MAP[ev.type];if(eventType&INPUT_START&&ev.button===0){this.pressed=true}if(eventType&INPUT_MOVE&&ev.which!==1){eventType=INPUT_END}if(!this.pressed||!this.allow){return}if(eventType&INPUT_END){this.pressed=false}this.callback(this.manager,eventType,{pointers:[ev],changedPointers:[ev],pointerType:INPUT_TYPE_MOUSE,srcEvent:ev})}});var POINTER_INPUT_MAP={pointerdown:INPUT_START,pointermove:INPUT_MOVE,pointerup:INPUT_END,pointercancel:INPUT_CANCEL,pointerout:INPUT_CANCEL};var IE10_POINTER_TYPE_ENUM={2:INPUT_TYPE_TOUCH,3:INPUT_TYPE_PEN,4:INPUT_TYPE_MOUSE,5:INPUT_TYPE_KINECT};var POINTER_ELEMENT_EVENTS="pointerdown";var POINTER_WINDOW_EVENTS="pointermove pointerup pointercancel";if(window.MSPointerEvent){POINTER_ELEMENT_EVENTS="MSPointerDown";POINTER_WINDOW_EVENTS="MSPointerMove MSPointerUp MSPointerCancel"}function PointerEventInput(){this.evEl=POINTER_ELEMENT_EVENTS;this.evWin=POINTER_WINDOW_EVENTS;Input.apply(this,arguments);this.store=this.manager.session.pointerEvents=[]}inherit(PointerEventInput,Input,{handler:function PEhandler(ev){var store=this.store;var removePointer=false;var eventTypeNormalized=ev.type.toLowerCase().replace("ms","");var eventType=POINTER_INPUT_MAP[eventTypeNormalized];var pointerType=IE10_POINTER_TYPE_ENUM[ev.pointerType]||ev.pointerType;var isTouch=pointerType==INPUT_TYPE_TOUCH;var storeIndex=inArray(store,ev.pointerId,"pointerId");if(eventType&INPUT_START&&(ev.button===0||isTouch)){if(storeIndex<0){store.push(ev);storeIndex=store.length-1}}else if(eventType&(INPUT_END|INPUT_CANCEL)){removePointer=true}if(storeIndex<0){return}store[storeIndex]=ev;this.callback(this.manager,eventType,{pointers:store,changedPointers:[ev],pointerType:pointerType,srcEvent:ev});if(removePointer){store.splice(storeIndex,1)}}});var SINGLE_TOUCH_INPUT_MAP={touchstart:INPUT_START,touchmove:INPUT_MOVE,touchend:INPUT_END,touchcancel:INPUT_CANCEL};var SINGLE_TOUCH_TARGET_EVENTS="touchstart";var SINGLE_TOUCH_WINDOW_EVENTS="touchstart touchmove touchend touchcancel";function SingleTouchInput(){this.evTarget=SINGLE_TOUCH_TARGET_EVENTS;this.evWin=SINGLE_TOUCH_WINDOW_EVENTS;this.started=false;Input.apply(this,arguments)}inherit(SingleTouchInput,Input,{handler:function TEhandler(ev){var type=SINGLE_TOUCH_INPUT_MAP[ev.type];if(type===INPUT_START){this.started=true}if(!this.started){return}var touches=normalizeSingleTouches.call(this,ev,type);if(type&(INPUT_END|INPUT_CANCEL)&&touches[0].length-touches[1].length===0){this.started=false}this.callback(this.manager,type,{pointers:touches[0],changedPointers:touches[1],pointerType:INPUT_TYPE_TOUCH,srcEvent:ev})}});function normalizeSingleTouches(ev,type){var all=toArray(ev.touches);var changed=toArray(ev.changedTouches);if(type&(INPUT_END|INPUT_CANCEL)){all=uniqueArray(all.concat(changed),"identifier",true)}return[all,changed]}var TOUCH_INPUT_MAP={touchstart:INPUT_START,touchmove:INPUT_MOVE,touchend:INPUT_END,touchcancel:INPUT_CANCEL};var TOUCH_TARGET_EVENTS="touchstart touchmove touchend touchcancel";function TouchInput(){this.evTarget=TOUCH_TARGET_EVENTS;this.targetIds={};Input.apply(this,arguments)}inherit(TouchInput,Input,{handler:function MTEhandler(ev){var type=TOUCH_INPUT_MAP[ev.type];var touches=getTouches.call(this,ev,type);if(!touches){return}this.callback(this.manager,type,{pointers:touches[0],changedPointers:touches[1],pointerType:INPUT_TYPE_TOUCH,srcEvent:ev})}});function getTouches(ev,type){var allTouches=toArray(ev.touches);var targetIds=this.targetIds;if(type&(INPUT_START|INPUT_MOVE)&&allTouches.length===1){targetIds[allTouches[0].identifier]=true;return[allTouches,allTouches]}var i,targetTouches,changedTouches=toArray(ev.changedTouches),changedTargetTouches=[],target=this.target;targetTouches=allTouches.filter(function(touch){return hasParent(touch.target,target)});if(type===INPUT_START){i=0;while(i-1){this.requireFail.splice(index,1)}return this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(otherRecognizer){return!!this.simultaneous[otherRecognizer.id]},emit:function(input){var self=this;var state=this.state;function emit(withState){self.manager.emit(self.options.event+(withState?stateStr(state):""),input)}if(state=STATE_ENDED){emit(true)}},tryEmit:function(input){if(this.canEmit()){return this.emit(input)}this.state=STATE_FAILED},canEmit:function(){var i=0;while(ioptions.threshold&&direction&options.direction},attrTest:function(input){return AttrRecognizer.prototype.attrTest.call(this,input)&&(this.state&STATE_BEGAN||!(this.state&STATE_BEGAN)&&this.directionTest(input))},emit:function(input){this.pX=input.deltaX;this.pY=input.deltaY;var direction=directionStr(input.direction);if(direction){this.manager.emit(this.options.event+direction,input)}this._super.emit.call(this,input)}});function PinchRecognizer(){AttrRecognizer.apply(this,arguments)}inherit(PinchRecognizer,AttrRecognizer,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[TOUCH_ACTION_NONE]},attrTest:function(input){return this._super.attrTest.call(this,input)&&(Math.abs(input.scale-1)>this.options.threshold||this.state&STATE_BEGAN)},emit:function(input){this._super.emit.call(this,input);if(input.scale!==1){var inOut=input.scale<1?"in":"out";this.manager.emit(this.options.event+inOut,input)}}});function PressRecognizer(){Recognizer.apply(this,arguments);this._timer=null;this._input=null}inherit(PressRecognizer,Recognizer,{defaults:{event:"press",pointers:1,time:500,threshold:5},getTouchAction:function(){return[TOUCH_ACTION_AUTO]},process:function(input){var options=this.options;var validPointers=input.pointers.length===options.pointers;var validMovement=input.distanceoptions.time;this._input=input;if(!validMovement||!validPointers||input.eventType&(INPUT_END|INPUT_CANCEL)&&!validTime){this.reset()}else if(input.eventType&INPUT_START){this.reset();this._timer=setTimeoutContext(function(){this.state=STATE_RECOGNIZED;this.tryEmit()},options.time,this)}else if(input.eventType&INPUT_END){return STATE_RECOGNIZED}return STATE_FAILED},reset:function(){clearTimeout(this._timer)},emit:function(input){if(this.state!==STATE_RECOGNIZED){return}if(input&&input.eventType&INPUT_END){this.manager.emit(this.options.event+"up",input)}else{this._input.timeStamp=now();this.manager.emit(this.options.event,this._input)}}});function RotateRecognizer(){AttrRecognizer.apply(this,arguments)}inherit(RotateRecognizer,AttrRecognizer,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[TOUCH_ACTION_NONE]},attrTest:function(input){return this._super.attrTest.call(this,input)&&(Math.abs(input.rotation)>this.options.threshold||this.state&STATE_BEGAN)}});function SwipeRecognizer(){AttrRecognizer.apply(this,arguments)}inherit(SwipeRecognizer,AttrRecognizer,{defaults:{event:"swipe",threshold:10,velocity:.65,direction:DIRECTION_HORIZONTAL|DIRECTION_VERTICAL,pointers:1},getTouchAction:function(){return PanRecognizer.prototype.getTouchAction.call(this)},attrTest:function(input){var direction=this.options.direction;var velocity;if(direction&(DIRECTION_HORIZONTAL|DIRECTION_VERTICAL)){velocity=input.velocity}else if(direction&DIRECTION_HORIZONTAL){velocity=input.velocityX}else if(direction&DIRECTION_VERTICAL){velocity=input.velocityY}return this._super.attrTest.call(this,input)&&direction&input.direction&&input.distance>this.options.threshold&&abs(velocity)>this.options.velocity&&input.eventType&INPUT_END},emit:function(input){var direction=directionStr(input.direction);if(direction){this.manager.emit(this.options.event+direction,input)}this.manager.emit(this.options.event,input)}});function TapRecognizer(){Recognizer.apply(this,arguments);this.pTime=false;this.pCenter=false;this._timer=null;this._input=null;this.count=0}inherit(TapRecognizer,Recognizer,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:2,posThreshold:10},getTouchAction:function(){return[TOUCH_ACTION_MANIPULATION]},process:function(input){var options=this.options;var validPointers=input.pointers.length===options.pointers;var validMovement=input.distance1?_len-1:0),_key=1;_key<_len;_key++){rest[_key-1]=arguments[_key]}return _standardEventTypes2["default"][eventType]?originalEvents.apply(undefined,[eventType].concat(rest)):fromHammerEvent.apply(undefined,[eventType,api].concat(rest))}}function override(obj,methodName,callback){obj[methodName]=callback(obj[methodName])}function selectCallback(originalSelect){return function select(){for(var _len2=arguments.length,args=Array(_len2),_key2=0;_key2<_len2;_key2++){args[_key2]=arguments[_key2]}var api=originalSelect.apply(this,args);override(api,"select",selectCallback);var originalEvents=api.events;api.events=makeEventsSelector(api,originalEvents);return api}}function hammerDriver(domDriverFunc,vTree$){if(typeof domDriverFunc!=="function"){throw new Error("First argument given to hammerDriver() must be a DOM driver function.")}if(!vTree$||typeof vTree$.subscribe!=="function"){throw new Error("The Hammer Driver expects a stream of virtual DOM elements as input.")}var domDriver=domDriverFunc(vTree$);override(domDriver,"select",selectCallback);return domDriver}exports["default"]=hammerDriver;module.exports=exports["default"]}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./fromEvent":3,"./standardEventTypes":6}],5:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}var _hammerDriver=require("./hammerDriver");var _hammerDriver2=_interopRequireDefault(_hammerDriver);function makeHammerDriver(domDriverFunc){if(typeof domDriverFunc!=="function"){throw new Error("First argument given to makeHammerDriver() "+"must be a DOM driver function.")}return _hammerDriver2["default"].bind({},domDriverFunc)}exports["default"]=makeHammerDriver;module.exports=exports["default"]},{"./hammerDriver":4}],6:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var standardEventTypes={abort:true,afterprint:true,animationend:true,animationiteration:true,animationstart:true,audioprocess:true,audioend:true,audiostart:true,beforeprint:true,beforeunload:true,beginEvent:true,blocked:true,blur:true,cached:true,canplay:true,canplaythrough:true,change:true,chargingchange:true,chargingtimechange:true,checking:true,click:true,close:true,complete:true,compositionend:true,compositionstart:true,compositionupdate:true,contextmenu:true,copy:true,cut:true,dblclick:true,devicelight:true,devicemotion:true,deviceorientation:true,deviceproximity:true,dischargingtimechange:true,DOMActivate:true,DOMAttributeNameChanged:true,DOMAttrModified:true,DOMCharacterDataModified:true,DOMContentLoaded:true,DOMElementNameChanged:true,DOMFocusIn:true,DOMFocusOut:true,DOMNodeInserted:true,DOMNodeInsertedIntoDocument:true,DOMNodeRemoved:true,DOMNodeRemovedFromDocument:true,DOMSubtreeModified:true,downloading:true,drag:true,dragend:true,dragenter:true,dragleave:true,dragover:true,dragstart:true,drop:true,durationchange:true,emptied:true,end:true,ended:true,endEvent:true,focus:true,focusin:true,focusout:true,fullscreenchange:true,fullscreenerror:true,gamepadconnected:true,gamepaddisconnected:true,gotpointercapture:true,hashchange:true,lostpointercapture:true,input:true,invalid:true,keydown:true,keypress:true,keyup:true,languagechange:true,levelchange:true,load:true,loadeddata:true,loadedmetadata:true,loadend:true,loadstart:true,message:true,mousedown:true,mouseenter:true,mouseleave:true,mousemove:true,mouseout:true,mouseover:true,mouseup:true,nomatch:true,notificationclick:true,noupdate:true,obsolete:true,offline:true,online:true,open:true,orientationchange:true,pagehide:true,pageshow:true,paste:true,pause:true,pointercancel:true,pointerdown:true,pointerenter:true,pointerleave:true,pointerlockchange:true,pointerlockerror:true,pointermove:true,pointerout:true,pointerover:true,pointerup:true,play:true,playing:true,popstate:true,progress:true,push:true,pushsubscriptionchange:true,ratechange:true,readystatechange:true,repeatEvent:true,reset:true,resize:true,result:true,scroll:true,seeked:true,seeking:true,select:true,selectstart:true,selectionchange:true,show:true,soundend:true,soundstart:true,speechend:true,speechstart:true,stalled:true,start:true,storage:true,submit:true,success:true,suspend:true,SVGAbort:true,SVGError:true,SVGLoad:true,SVGResize:true,SVGScroll:true,SVGUnload:true,SVGZoom:true,timeout:true,timeupdate:true,touchcancel:true,touchend:true,touchenter:true,touchleave:true,touchmove:true,touchstart:true,transitionend:true,unload:true,updateready:true,upgradeneeded:true,userproximity:true,versionchange:true,visibilitychange:true,volumechange:true,waiting:true,wheel:true};exports["default"]=standardEventTypes;module.exports=exports["default"]},{}]},{},[2])(2)}); -------------------------------------------------------------------------------- /doc/api.md: -------------------------------------------------------------------------------- 1 | 2 | # Cyclic Materials Cycle Hammer Driver API 3 | 4 | - [`makeDOMDriver`](#makeDOMDriver) 5 | 6 | Author: Frederik Krautwald 7 | 8 | - - - 9 | 10 | ### `makeDOMDriver(domDriverFunc)` 11 | 12 | A factory for the Hammer driver function. Takes a DOM driver function that 13 | adheres to the interface established by the Cycle.js DOM driver. 14 | The output (source) of this driver mimics the DOM driver as a collection 15 | of Observables queried with: `DOM.select(selector).events(eventType)`. 16 | In addition, when listening for hammer.js `eventTypes`, the `events()` 17 | function allows you to specify an optional callback: 18 | `DOM.select(selector).events(eventType, callback)`. 19 | The `callback` takes two arguments: 20 | 21 | - **manager** `{Object}` The `Hammer.Manager` instance for the element. 22 | - **Hammer** `{Object}` The Hammer API. 23 | 24 | #### Arguments: 25 | 26 | - `domDriverFunc :: Function` the DOM driver function. 27 | 28 | #### Return: 29 | 30 | *(Function)* the Hammer driver function. The function expects an Observable of VTree as input, and outputs the source object for this 31 | driver, containing functions `select()` and `dispose()` that can be used 32 | for debugging and testing. 33 | 34 | - - - 35 | 36 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | Basic cycle-hammer-driver example 10 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/basic/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Demo dataflow component 3 | * @author Frederik Krautwald 4 | */ 5 | 6 | import {div, h3, h4} from '@cycle/dom' 7 | import {Observable} from 'rx' 8 | 9 | function Demo(sources) { 10 | const intent = ({dom}) => { 11 | const options = (manager, Hammer) => { 12 | // Default pan recognizer. 13 | manager.add(new Hammer.Pan()) 14 | // Default tap recognizer. 15 | manager.add(new Hammer.Tap()) 16 | // Default press recognizer. 17 | manager.add(new Hammer.Press()) 18 | } 19 | return { 20 | events$: Observable.merge( 21 | dom.select(`#MyElement`).events(`panleft`, options), 22 | dom.select(`#MyElement`).events(`panright`), 23 | dom.select(`#MyElement`).events(`tap`), 24 | dom.select(`#MyElement`).events(`press`) 25 | ), 26 | } 27 | } 28 | 29 | const model = (actions) => { 30 | return actions.events$.map( 31 | event => ({event}) 32 | ).startWith({event: {type: ``}}) 33 | } 34 | 35 | const view = (state$) => state$.map( 36 | state => { 37 | const {type} = state.event 38 | return div([ 39 | h3(`Basic cycle-hammer-driver example`), 40 | h4(`Events: panleft panright tap press`), 41 | div(`#MyElement`, `${type} gesture detected`) 42 | ]) 43 | } 44 | ) 45 | 46 | const actions = intent(sources) 47 | const state$ = model(actions) 48 | 49 | return { 50 | dom: view(state$), 51 | } 52 | } 53 | 54 | export default Demo 55 | -------------------------------------------------------------------------------- /examples/basic/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Demo main 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | import {run} from '@cycle/core' 8 | import {makeDOMDriver as makeDomDriver} from '@cycle/dom' 9 | import makeHammerDriver from './../../src/makeHammerDriver' 10 | import Demo from './index' 11 | 12 | const main = Demo 13 | 14 | const domDriverFunc = makeDomDriver(`.demo-container`) 15 | 16 | run(main, { 17 | dom: makeHammerDriver(domDriverFunc), 18 | }) 19 | -------------------------------------------------------------------------------- /examples/quadruple-tap/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | Quadruple Tap cycle-hammer-driver example 10 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/quadruple-tap/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Demo dataflow component 3 | * @author Frederik Krautwald 4 | */ 5 | 6 | import {div, h3, h4} from '@cycle/dom' 7 | import {Observable} from 'rx' 8 | 9 | function Demo(sources) { 10 | const intent = ({dom}) => { 11 | const options = (manager, Hammer) => { 12 | // Default tap recognizer. 13 | manager.add(new Hammer.Tap()) 14 | 15 | // Tap recognizer with four minimal taps. 16 | manager.add(new Hammer.Tap({event: `quadrupletap`, taps: 4})) 17 | 18 | // We want to recognize this simultaneous, so a quadruple tap will be 19 | // detected even while a tap is being recognized. 20 | // The tap event will be emitted on every tap. 21 | manager.get(`quadrupletap`).recognizeWith(`tap`) 22 | } 23 | return { 24 | event$: Observable.merge( 25 | dom.select(`#MyElement`).events(`tap`, options), 26 | dom.select(`#MyElement`).events(`quadrupletap`) 27 | ), 28 | } 29 | } 30 | 31 | const model = (actions) => { 32 | return actions.event$.map( 33 | event => ({event}) 34 | ).startWith({event: {type: ``}}) 35 | } 36 | 37 | const view = (state$) => { 38 | let types = `` 39 | return state$.map( 40 | state => { 41 | const {event} = state 42 | types += `${event.type} ` 43 | return div([ 44 | h3(`Quadruple Tap cycle-hammer-driver example`), 45 | h4(`Events: tap, quadrupletap`), 46 | div(`#MyElement`, `${types}`), 47 | ]) 48 | } 49 | ) 50 | } 51 | 52 | const actions = intent(sources) 53 | const state$ = model(actions) 54 | 55 | return { 56 | dom: view(state$), 57 | } 58 | } 59 | 60 | export default Demo 61 | -------------------------------------------------------------------------------- /examples/quadruple-tap/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Demo main 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | import {run} from '@cycle/core' 8 | import {makeDOMDriver as makeDomDriver} from '@cycle/dom' 9 | import makeHammerDriver from './../../src/makeHammerDriver' 10 | import Demo from './index' 11 | 12 | const main = Demo 13 | 14 | const domDriverFunc = makeDomDriver(`.demo-container`) 15 | 16 | run(main, { 17 | dom: makeHammerDriver(domDriverFunc), 18 | }) 19 | -------------------------------------------------------------------------------- /examples/recognizeWith-requireFailure-taps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | RecognizeWith and requireFailure cycle-hammer-driver taps example 10 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/recognizeWith-requireFailure-taps/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Demo dataflow component 3 | * @author Frederik Krautwald 4 | */ 5 | 6 | import {div, h3, h4} from '@cycle/dom' 7 | import {Observable} from 'rx' 8 | 9 | function Demo(sources) { 10 | const intent = ({dom}) => { 11 | const options = (manager, Hammer) => { 12 | // Tap recognizer with minimal two taps. 13 | manager.add(new Hammer.Tap({event: `doubletap`, taps: 2})) 14 | // Single tap recognizer. 15 | manager.add(new Hammer.Tap({event: `singletap`})) 16 | 17 | // We want to recognize this simultaneous, so a double tap will be 18 | // detected even while a single tap is being recognized. 19 | // The tap event will be emitted on every tap. 20 | manager.get(`doubletap`).recognizeWith(`singletap`) 21 | // We only want to trigger a tap when we haven’t detected a double tap. 22 | manager.get(`singletap`).requireFailure(`doubletap`) 23 | } 24 | return { 25 | event$: Observable.merge( 26 | dom.select(`#MyElement`).events(`singletap`, options), 27 | dom.select(`#MyElement`).events(`doubletap`) 28 | ), 29 | } 30 | } 31 | 32 | const model = (actions) => { 33 | return actions.event$.map( 34 | event => ({event}) 35 | ).startWith({event: {type: ``}}) 36 | } 37 | 38 | const view = (state$) => { 39 | let types = `` 40 | return state$.map( 41 | state => { 42 | const {event} = state 43 | types += `${event.type} ` 44 | return div([ 45 | h3(`RecognizeWith and requireFailure cycle-hammer-driver taps example`), 46 | h4(`Events: singletap, doubletap`), 47 | div(`#MyElement`, `${types}`), 48 | ]) 49 | } 50 | ) 51 | } 52 | 53 | const actions = intent(sources) 54 | const state$ = model(actions) 55 | 56 | return { 57 | dom: view(state$), 58 | } 59 | } 60 | 61 | export default Demo 62 | -------------------------------------------------------------------------------- /examples/recognizeWith-requireFailure-taps/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Demo main 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | import {run} from '@cycle/core' 8 | import {makeDOMDriver as makeDomDriver} from '@cycle/dom' 9 | import makeHammerDriver from './../../src/makeHammerDriver' 10 | import Demo from './index' 11 | 12 | const main = Demo 13 | 14 | const domDriverFunc = makeDomDriver(`.demo-container`) 15 | 16 | run(main, { 17 | dom: makeHammerDriver(domDriverFunc), 18 | }) 19 | -------------------------------------------------------------------------------- /examples/recognizeWith/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | RecognizeWith cycle-hammer-driver example 10 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/recognizeWith/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Demo dataflow component 3 | * @author Frederik Krautwald 4 | */ 5 | 6 | import {div, h3, h4} from '@cycle/dom' 7 | import {Observable} from 'rx' 8 | 9 | function Demo(sources) { 10 | const intent = ({dom}) => { 11 | const options = (manager, Hammer) => { 12 | // create a pinch and rotate recognizer 13 | // these require two pointers 14 | const pinch = new Hammer.Pinch() 15 | const rotate = new Hammer.Rotate() 16 | 17 | // we want to detect both at the same time 18 | pinch.recognizeWith(rotate) 19 | 20 | // add to the Manager 21 | manager.add([pinch, rotate]) 22 | } 23 | return { 24 | event$: Observable.merge( 25 | dom.select(`#MyElement`).events(`pinch`, options), 26 | dom.select(`#MyElement`).events(`rotate`) 27 | ), 28 | } 29 | } 30 | 31 | const model = (actions) => { 32 | return actions.event$.map( 33 | event => ({event}) 34 | ).startWith({event: {type: ``}}) 35 | } 36 | 37 | const view = (state$) => { 38 | let types = `` 39 | return state$.map( 40 | state => { 41 | const {event} = state 42 | types += `${event.type} ` 43 | return div([ 44 | h3(`RecognizeWith cycle-hammer-driver example`), 45 | h4(`Events: pinch, rotate`), 46 | div(`#MyElement`, `${types}`), 47 | ]) 48 | } 49 | ) 50 | } 51 | 52 | const actions = intent(sources) 53 | const state$ = model(actions) 54 | 55 | return { 56 | dom: view(state$), 57 | } 58 | } 59 | 60 | export default Demo 61 | -------------------------------------------------------------------------------- /examples/recognizeWith/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Demo main 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | import {run} from '@cycle/core' 8 | import {makeDOMDriver as makeDomDriver} from '@cycle/dom' 9 | import makeHammerDriver from './../../src/makeHammerDriver' 10 | import Demo from './index' 11 | 12 | const main = Demo 13 | 14 | const domDriverFunc = makeDomDriver(`.demo-container`) 15 | 16 | run(main, { 17 | dom: makeHammerDriver(domDriverFunc), 18 | }) 19 | -------------------------------------------------------------------------------- /examples/vertical-pan/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | Vertical Pan cycle-hammer-driver example 10 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/vertical-pan/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Demo dataflow component 3 | * @author Frederik Krautwald 4 | */ 5 | 6 | import {div, h3, h4} from '@cycle/dom' 7 | import {Observable} from 'rx' 8 | 9 | function Demo(sources) { 10 | const intent = ({dom}) => { 11 | const options = (manager, Hammer) => { 12 | // Default pan recognizer. 13 | manager.add(new Hammer.Pan()) 14 | // Default tap recognizer. 15 | manager.add(new Hammer.Tap()) 16 | // Default press recognizer. 17 | manager.add(new Hammer.Press()) 18 | // Let the pan gesture support all directions. 19 | // This will block the vertical scrolling on a touch-device 20 | // while on the element. 21 | manager.get(`pan`).set({direction: Hammer.DIRECTION_ALL}) 22 | } 23 | return { 24 | events$: Observable 25 | .merge( 26 | dom.select(`#MyElement`).events(`panleft`, options), 27 | dom.select(`#MyElement`).events(`panright`), 28 | dom.select(`#MyElement`).events(`panup`), 29 | dom.select(`#MyElement`).events(`pandown`), 30 | dom.select(`#MyElement`).events(`tap`), 31 | dom.select(`#MyElement`).events(`press`) 32 | ), 33 | } 34 | } 35 | 36 | const model = (actions) => { 37 | return actions.events$.map( 38 | event => ({event}) 39 | ).startWith({event: {type: ``}}) 40 | } 41 | 42 | const view = (state$) => state$.map( 43 | state => { 44 | const {type} = state.event 45 | return div([ 46 | h3(`Vertical Pan cycle-hammer-driver example`), 47 | h4(`Events: panleft panright panup pandown tap press`), 48 | div(`#MyElement`, `${type} gesture detected`), 49 | ]) 50 | } 51 | ) 52 | 53 | const actions = intent(sources) 54 | const state$ = model(actions) 55 | 56 | return { 57 | dom: view(state$), 58 | } 59 | } 60 | 61 | export default Demo 62 | -------------------------------------------------------------------------------- /examples/vertical-pan/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Demo main 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | import {run} from '@cycle/core' 8 | import {makeDOMDriver as makeDomDriver} from '@cycle/dom' 9 | import makeHammerDriver from './../../src/makeHammerDriver' 10 | import Demo from './index' 11 | 12 | const main = Demo 13 | 14 | const domDriverFunc = makeDomDriver(`.demo-container`) 15 | 16 | run(main, { 17 | dom: makeHammerDriver(domDriverFunc), 18 | }) 19 | -------------------------------------------------------------------------------- /lib/cycle-hammer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Hammer driver for Cycle.js 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | 'use strict'; 8 | 9 | Object.defineProperty(exports, '__esModule', { 10 | value: true 11 | }); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 14 | 15 | var _makeHammerDriver = require('./makeHammerDriver'); 16 | 17 | var _makeHammerDriver2 = _interopRequireDefault(_makeHammerDriver); 18 | 19 | var CycleHammer = { 20 | /** 21 | * A factory for the Hammer driver function. Takes a DOM driver function that 22 | * adheres to the interface established by the Cycle.js DOM driver. 23 | * The output (source) of this driver mimics the DOM driver as a collection 24 | * of Observables queried with: `DOM.select(selector).events(eventType)`. 25 | * In addition, when listening for hammer.js `eventTypes`, the `events()` 26 | * function allows you to specify an optional callback: 27 | * `DOM.select(selector).events(eventType, callback)`. 28 | * The `callback` takes two arguments: 29 | * 30 | * - **manager** `{Object}` The `Hammer.Manager` instance for the element. 31 | * - **Hammer** `{Object}` The Hammer API. 32 | * 33 | * @param {Function} domDriverFunc the DOM driver function. 34 | * @return {Function} the Hammer driver function. The function expects an 35 | * Observable of VTree as input, and outputs the source object for this 36 | * driver, containing functions `select()` and `dispose()` that can be used 37 | * for debugging and testing. 38 | * @function makeDOMDriver 39 | */ 40 | makeHammerDriver: _makeHammerDriver2['default'] 41 | }; 42 | 43 | exports['default'] = CycleHammer; 44 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/fromEvent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Observable from Hammer Event 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Frederik Krautwald. All rights reserved. 5 | */ 6 | 7 | 'use strict'; 8 | 9 | Object.defineProperty(exports, '__esModule', { 10 | value: true 11 | }); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 14 | 15 | var _rx = require('rx'); 16 | 17 | var _hammerjs = require('hammerjs'); 18 | 19 | var _hammerjs2 = _interopRequireDefault(_hammerjs); 20 | 21 | var hammers = {}; 22 | 23 | function createListener(_ref) { 24 | var element = _ref.element; 25 | var eventType = _ref.eventType; 26 | var handler = _ref.handler; 27 | var k = _ref.k; 28 | var _ref$callback = _ref.callback; 29 | var callback = _ref$callback === undefined ? function () {} : _ref$callback; 30 | 31 | if (typeof hammers[k] !== 'object') { 32 | hammers[k] = new _hammerjs2['default'].Manager(element); 33 | } 34 | var hammer = hammers[k]; 35 | callback(hammer, _hammerjs2['default']); 36 | hammer.on(eventType, handler); 37 | } 38 | 39 | function createEventListener(_ref2) { 40 | var element = _ref2.element; 41 | var eventType = _ref2.eventType; 42 | var handler = _ref2.handler; 43 | var k = _ref2.k; 44 | var callback = _ref2.callback; 45 | 46 | if (Array.isArray(element)) { 47 | for (var i = 0, len = element.length; i < len; i++) { 48 | createEventListener({ 49 | element: element[i], 50 | eventType: eventType, 51 | handler: handler, 52 | k: k, 53 | callback: callback 54 | }); 55 | } 56 | } else if (element) { 57 | createListener({ element: element, eventType: eventType, handler: handler, k: k, callback: callback }); 58 | } 59 | } 60 | 61 | var elementEvents = {}; 62 | 63 | function fromEvent(_ref3) { 64 | var element = _ref3.element; 65 | var eventType = _ref3.eventType; 66 | var k = _ref3.k; 67 | var callback = _ref3.callback; 68 | 69 | if (typeof elementEvents[k] !== 'object') { 70 | elementEvents[k] = {}; 71 | } 72 | var elementEvent = elementEvents[k]; 73 | var event$ = undefined; 74 | if (elementEvent[eventType]) { 75 | event$ = elementEvent[eventType]; 76 | } else { 77 | elementEvent[eventType] = new _rx.Subject(); 78 | event$ = elementEvent[eventType]; 79 | createEventListener({ 80 | element: element, 81 | eventType: eventType, 82 | handler: function handler(value) { 83 | //console.log(`handler`, value.type) 84 | event$.onNext(value); 85 | }, 86 | k: k, 87 | callback: callback 88 | }); 89 | } 90 | return event$; 91 | } 92 | 93 | exports['default'] = fromEvent; 94 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/hammerDriver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Hammer driver 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | 'use strict'; 8 | 9 | Object.defineProperty(exports, '__esModule', { 10 | value: true 11 | }); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 14 | 15 | var _rx = require('rx'); 16 | 17 | var _fromEvent = require('./fromEvent'); 18 | 19 | var _fromEvent2 = _interopRequireDefault(_fromEvent); 20 | 21 | var _standardEventTypes = require('./standardEventTypes'); 22 | 23 | var _standardEventTypes2 = _interopRequireDefault(_standardEventTypes); 24 | 25 | function fromHammerEvent(eventType, api, callback) { 26 | return api.observable.flatMapLatest(function (element) { 27 | if (!element) { 28 | return _rx.Observable.empty(); 29 | } 30 | var k = api.namespace.join(''); 31 | return (0, _fromEvent2['default'])({ element: element, eventType: eventType, k: k, callback: callback }); 32 | }).share(); 33 | } 34 | 35 | function makeEventsSelector(api, originalEvents) { 36 | return function events(eventType) { 37 | for (var _len = arguments.length, rest = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 38 | rest[_key - 1] = arguments[_key]; 39 | } 40 | 41 | return _standardEventTypes2['default'][eventType] ? originalEvents.apply(undefined, [eventType].concat(rest)) : fromHammerEvent.apply(undefined, [eventType, api].concat(rest)); 42 | }; 43 | } 44 | 45 | function override(obj, methodName, callback) { 46 | obj[methodName] = callback(obj[methodName]); 47 | } 48 | 49 | function selectCallback(originalSelect) { 50 | return function select() { 51 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 52 | args[_key2] = arguments[_key2]; 53 | } 54 | 55 | var api = originalSelect.apply(this, args); 56 | override(api, 'select', selectCallback); 57 | var originalEvents = api.events; 58 | api.events = makeEventsSelector(api, originalEvents); 59 | return api; 60 | }; 61 | } 62 | 63 | function hammerDriver(domDriverFunc, vTree$) { 64 | if (typeof domDriverFunc !== 'function') { 65 | throw new Error('First argument given to hammerDriver() must be a DOM driver function.'); 66 | } 67 | if (!vTree$ || typeof vTree$.subscribe !== 'function') { 68 | throw new Error('The Hammer Driver expects a stream of virtual DOM elements as input.'); 69 | } 70 | 71 | var domDriver = domDriverFunc(vTree$); 72 | override(domDriver, 'select', selectCallback); 73 | 74 | return domDriver; 75 | } 76 | 77 | exports['default'] = hammerDriver; 78 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/makeHammerDriver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Hammer driver factory 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | 'use strict'; 8 | 9 | Object.defineProperty(exports, '__esModule', { 10 | value: true 11 | }); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 14 | 15 | var _hammerDriver = require('./hammerDriver'); 16 | 17 | var _hammerDriver2 = _interopRequireDefault(_hammerDriver); 18 | 19 | function makeHammerDriver(domDriverFunc) { 20 | if (typeof domDriverFunc !== 'function') { 21 | throw new Error('First argument given to makeHammerDriver() ' + 'must be a DOM driver function.'); 22 | } 23 | return _hammerDriver2['default'].bind({}, domDriverFunc); 24 | } 25 | 26 | exports['default'] = makeHammerDriver; 27 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/standardEventTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Standard Event Types 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | // https://developer.mozilla.org/en-US/docs/Web/Events 8 | "use strict"; 9 | 10 | Object.defineProperty(exports, "__esModule", { 11 | value: true 12 | }); 13 | var standardEventTypes = { 14 | abort: true, 15 | afterprint: true, 16 | animationend: true, 17 | animationiteration: true, 18 | animationstart: true, 19 | audioprocess: true, 20 | audioend: true, 21 | audiostart: true, 22 | beforeprint: true, 23 | beforeunload: true, 24 | beginEvent: true, 25 | blocked: true, 26 | blur: true, 27 | cached: true, 28 | canplay: true, 29 | canplaythrough: true, 30 | change: true, 31 | chargingchange: true, 32 | chargingtimechange: true, 33 | checking: true, 34 | click: true, 35 | close: true, 36 | complete: true, 37 | compositionend: true, 38 | compositionstart: true, 39 | compositionupdate: true, 40 | contextmenu: true, 41 | copy: true, 42 | cut: true, 43 | dblclick: true, 44 | devicelight: true, 45 | devicemotion: true, 46 | deviceorientation: true, 47 | deviceproximity: true, 48 | dischargingtimechange: true, 49 | DOMActivate: true, 50 | DOMAttributeNameChanged: true, 51 | DOMAttrModified: true, 52 | DOMCharacterDataModified: true, 53 | DOMContentLoaded: true, 54 | DOMElementNameChanged: true, 55 | DOMFocusIn: true, 56 | DOMFocusOut: true, 57 | DOMNodeInserted: true, 58 | DOMNodeInsertedIntoDocument: true, 59 | DOMNodeRemoved: true, 60 | DOMNodeRemovedFromDocument: true, 61 | DOMSubtreeModified: true, 62 | downloading: true, 63 | drag: true, 64 | dragend: true, 65 | dragenter: true, 66 | dragleave: true, 67 | dragover: true, 68 | dragstart: true, 69 | drop: true, 70 | durationchange: true, 71 | emptied: true, 72 | end: true, 73 | ended: true, 74 | endEvent: true, 75 | focus: true, 76 | focusin: true, 77 | focusout: true, 78 | fullscreenchange: true, 79 | fullscreenerror: true, 80 | gamepadconnected: true, 81 | gamepaddisconnected: true, 82 | gotpointercapture: true, 83 | hashchange: true, 84 | lostpointercapture: true, 85 | input: true, 86 | invalid: true, 87 | keydown: true, 88 | keypress: true, 89 | keyup: true, 90 | languagechange: true, 91 | levelchange: true, 92 | load: true, 93 | loadeddata: true, 94 | loadedmetadata: true, 95 | loadend: true, 96 | loadstart: true, 97 | message: true, 98 | mousedown: true, 99 | mouseenter: true, 100 | mouseleave: true, 101 | mousemove: true, 102 | mouseout: true, 103 | mouseover: true, 104 | mouseup: true, 105 | nomatch: true, 106 | notificationclick: true, 107 | noupdate: true, 108 | obsolete: true, 109 | offline: true, 110 | online: true, 111 | open: true, 112 | orientationchange: true, 113 | pagehide: true, 114 | pageshow: true, 115 | paste: true, 116 | pause: true, 117 | pointercancel: true, 118 | pointerdown: true, 119 | pointerenter: true, 120 | pointerleave: true, 121 | pointerlockchange: true, 122 | pointerlockerror: true, 123 | pointermove: true, 124 | pointerout: true, 125 | pointerover: true, 126 | pointerup: true, 127 | play: true, 128 | playing: true, 129 | popstate: true, 130 | progress: true, 131 | push: true, 132 | pushsubscriptionchange: true, 133 | ratechange: true, 134 | readystatechange: true, 135 | repeatEvent: true, 136 | reset: true, 137 | resize: true, 138 | result: true, 139 | scroll: true, 140 | seeked: true, 141 | seeking: true, 142 | select: true, 143 | selectstart: true, 144 | selectionchange: true, 145 | show: true, 146 | soundend: true, 147 | soundstart: true, 148 | speechend: true, 149 | speechstart: true, 150 | stalled: true, 151 | start: true, 152 | storage: true, 153 | submit: true, 154 | success: true, 155 | suspend: true, 156 | SVGAbort: true, 157 | SVGError: true, 158 | SVGLoad: true, 159 | SVGResize: true, 160 | SVGScroll: true, 161 | SVGUnload: true, 162 | SVGZoom: true, 163 | timeout: true, 164 | timeupdate: true, 165 | touchcancel: true, 166 | touchend: true, 167 | touchenter: true, 168 | touchleave: true, 169 | touchmove: true, 170 | touchstart: true, 171 | transitionend: true, 172 | unload: true, 173 | updateready: true, 174 | upgradeneeded: true, 175 | userproximity: true, 176 | versionchange: true, 177 | visibilitychange: true, 178 | volumechange: true, 179 | waiting: true, 180 | wheel: true 181 | }; 182 | 183 | //const hammerEventTable = { 184 | // pan: true, 185 | // panstart: true, 186 | // panmove: true, 187 | // panend: true, 188 | // pancancel: true, 189 | // panleft: true, 190 | // panright: true, 191 | // panup: true, 192 | // pandown: true, 193 | // pinch: true, 194 | // pinchstart: true, 195 | // pinchmove: true, 196 | // pinchend: true, 197 | // pinchcancel: true, 198 | // pinchin: true, 199 | // pinchout: true, 200 | // press: true, 201 | // pressup: true, 202 | // rotate: true, 203 | // rotatestart: true, 204 | // rotatemove: true, 205 | // rotateend: true, 206 | // rotatecancel: true, 207 | // swipe: true, 208 | // swipeleft: true, 209 | // swiperight: true, 210 | // swipeup: true, 211 | // swipedown: true, 212 | // tap: true, 213 | //} 214 | 215 | exports["default"] = standardEventTypes; 216 | module.exports = exports["default"]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cyclic/cycle-hammer-driver", 3 | "version": "0.1.1", 4 | "author": "Frederik Krautwald", 5 | "description": "A Hammer driver for Cycle.js", 6 | "license": "MIT", 7 | "bugs": "https://github.com/CyclicMaterials/cycle-hammer-driver/issues", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/CyclicMaterials/cycle-hammer-driver" 11 | }, 12 | "keywords": [ 13 | "cyclejs", 14 | "driver", 15 | "hammer", 16 | "hammerjs", 17 | "gesture", 18 | "events", 19 | "dom" 20 | ], 21 | "directories": { 22 | "doc": "./doc", 23 | "lib": "./lib" 24 | }, 25 | "files": [ 26 | "lib/" 27 | ], 28 | "main": "lib/cycle-hammer.js", 29 | "dependencies": { 30 | "hammerjs": "2.0.4" 31 | }, 32 | "devDependencies": { 33 | "@cycle/core": "^6.0.0-rc2", 34 | "@cycle/dom": "^8.0.0-rc2", 35 | "babel": "5.8.29", 36 | "babelify": "6.4.0", 37 | "browserify": "12.0.1", 38 | "browserify-shim": "3.8.11", 39 | "chai": "3.4.1", 40 | "chai-virtual-dom": "1.0.0", 41 | "cli-release": "1.0.3", 42 | "eslint": "1.0.0", 43 | "eslint-config-cycle": "3.1.0", 44 | "eslint-plugin-cycle": "1.0.2", 45 | "eslint-plugin-no-class": "0.1.0", 46 | "markdox": "0.1.10", 47 | "mocha": "2.3.4", 48 | "rx": "4.0.7", 49 | "testem": "0.9.11", 50 | "uglify-js": "2.6.1", 51 | "validate-commit-message": "3.0.1", 52 | "virtual-dom": "2.1.1" 53 | }, 54 | "browserify-shim": { 55 | "rx": "global:Rx" 56 | }, 57 | "scripts": { 58 | "lint": "eslint src/", 59 | "examples": "node scripts/build-examples.js basic,vertical-pan,recognizeWith,quadruple-tap,recognizeWith-requireFailure-taps", 60 | "test-browser": "testem", 61 | "pretest-browser": "npm run build-test", 62 | "posttest-browser": "npm run clean-test", 63 | "build-test": "browserify test/index.js -t babelify -o test/page/tests-bundle.js", 64 | "clean-test": "rm test/page/tests-bundle.js", 65 | "test-ci": "testem ci", 66 | "test": "npm run lint && npm run test-ci", 67 | "deps": "npm install && validate-commit-msg", 68 | "start": "npm run deps", 69 | "browserify": "browserify src/cycle-hammer.js -t babelify -t browserify-shim --standalone CycleHammer --exclude rx --outfile dist/cycle-hammer.js", 70 | "uglify": "uglifyjs dist/cycle-hammer.js -o dist/cycle-hammer.min.js", 71 | "dist": "npm run browserify && npm run uglify", 72 | "predist": "rm -rf dist/ && mkdir -p dist", 73 | "doc": "node ./scripts/make-api-doc.js", 74 | "predoc": "rm -rf doc/ && mkdir -p doc", 75 | "compile": "babel -d lib/ src/", 76 | "precompile": "rm -rf lib/ && mkdir -p lib", 77 | "prepublish": "npm run compile", 78 | "patch": "git checkout master && release patch && npm publish --access=public", 79 | "minor": "git checkout master && release minor && npm publish --access=public", 80 | "major": "git checkout master && release major && npm publish --access=public" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /scripts/build-examples.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Build examples 3 | * @author Frederik Krautwald 4 | */ 5 | 6 | const fs = require(`fs`) 7 | const path = require(`path`) 8 | const browserify = require(`browserify`) 9 | 10 | const baseDir = `examples${path.sep}` 11 | const exampleDirs = process.argv[2].split(`,`) 12 | 13 | function transpile(exampleDir) { 14 | const dir = baseDir + exampleDir + path.sep 15 | const file = `${dir}main.js` 16 | const outfile = `${dir}bundle.js` 17 | browserify(file) 18 | .transform(`babelify`) 19 | .bundle() 20 | .pipe(fs.createWriteStream(outfile)) 21 | } 22 | 23 | exampleDirs.forEach(transpile) 24 | -------------------------------------------------------------------------------- /scripts/cyclic-docs-template.md.ejs: -------------------------------------------------------------------------------- 1 | 2 | # Cyclic Materials Cycle Hammer Driver API 3 | 4 | 5 | 6 | 7 | - [``](#) 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ### `()` 17 | 18 | ### `` 19 | 20 | 21 | 22 | 23 | 24 | 25 | **Deprecated** 26 | 27 | 28 | 29 | Author: 30 | 31 | 32 | 33 | Version: 34 | 35 | 36 | 37 | See: 38 | 39 | 40 | 0) { ?> 41 | #### Arguments: 42 | 43 | - ` :: ` 44 | 45 | 46 | 0) { ?> 47 | #### Return: 48 | 49 | *()* 50 | 51 | 52 | 53 | 54 | - - - 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /scripts/make-api-doc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | 'use strict'; 4 | var markdox = require('markdox'); 5 | 6 | function generateDoc(options) { 7 | markdox.process( 8 | options.src, 9 | {output: options.output, template: options.template}, 10 | function generationCallback(err/*, output */) { 11 | if (err) { 12 | console.error(err); 13 | } else { 14 | console.log('File `' + options.output + '` generated with success'); 15 | } 16 | } 17 | ); 18 | } 19 | 20 | generateDoc({ 21 | src: [ 22 | './src/cycle-hammer.js', 23 | ], 24 | output: './doc/api.md', 25 | template: './scripts/cyclic-docs-template.md.ejs' 26 | }); 27 | -------------------------------------------------------------------------------- /src/cycle-hammer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Hammer driver for Cycle.js 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | import makeHammerDriver from './makeHammerDriver' 8 | 9 | const CycleHammer = { 10 | /** 11 | * A factory for the Hammer driver function. Takes a DOM driver function that 12 | * adheres to the interface established by the Cycle.js DOM driver. 13 | * The output (source) of this driver mimics the DOM driver as a collection 14 | * of Observables queried with: `DOM.select(selector).events(eventType)`. 15 | * In addition, when listening for hammer.js `eventTypes`, the `events()` 16 | * function allows you to specify an optional callback: 17 | * `DOM.select(selector).events(eventType, callback)`. 18 | * The `callback` takes two arguments: 19 | * 20 | * - **manager** `{Object}` The `Hammer.Manager` instance for the element. 21 | * - **Hammer** `{Object}` The Hammer API. 22 | * 23 | * @param {Function} domDriverFunc the DOM driver function. 24 | * @return {Function} the Hammer driver function. The function expects an 25 | * Observable of VTree as input, and outputs the source object for this 26 | * driver, containing functions `select()` and `dispose()` that can be used 27 | * for debugging and testing. 28 | * @function makeDOMDriver 29 | */ 30 | makeHammerDriver, 31 | } 32 | 33 | export default CycleHammer 34 | -------------------------------------------------------------------------------- /src/fromEvent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Observable from Hammer Event 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Frederik Krautwald. All rights reserved. 5 | */ 6 | 7 | import {Subject} from 'rx' 8 | import Hammer from 'hammerjs' 9 | 10 | const hammers = {} 11 | 12 | function createListener( 13 | {element, eventType, handler, k, callback = () => {}} 14 | ) { 15 | if (typeof hammers[k] !== `object`) { 16 | hammers[k] = new Hammer.Manager(element) 17 | } 18 | const hammer = hammers[k] 19 | callback(hammer, Hammer) 20 | hammer.on(eventType, handler) 21 | } 22 | 23 | function createEventListener( 24 | {element, eventType, handler, k, callback} 25 | ) { 26 | if (Array.isArray(element)) { 27 | for (let i = 0, len = element.length; i < len; i++) { 28 | createEventListener({ 29 | element: element[i], 30 | eventType, 31 | handler, 32 | k, 33 | callback, 34 | }) 35 | } 36 | } else if (element) { 37 | createListener({element, eventType, handler, k, callback}) 38 | } 39 | } 40 | 41 | const elementEvents = {} 42 | 43 | function fromEvent({element, eventType, k, callback}) { 44 | if (typeof elementEvents[k] !== `object`) { 45 | elementEvents[k] = {} 46 | } 47 | const elementEvent = elementEvents[k] 48 | let event$ 49 | if (elementEvent[eventType]) { 50 | event$ = elementEvent[eventType] 51 | } else { 52 | elementEvent[eventType] = new Subject() 53 | event$ = elementEvent[eventType] 54 | createEventListener({ 55 | element, 56 | eventType, 57 | handler: (value) => { 58 | //console.log(`handler`, value.type) 59 | event$.onNext(value) 60 | }, 61 | k, 62 | callback, 63 | }) 64 | } 65 | return event$ 66 | } 67 | 68 | export default fromEvent 69 | -------------------------------------------------------------------------------- /src/hammerDriver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Hammer driver 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | import {Observable} from 'rx' 8 | import fromEvent from './fromEvent' 9 | import eventTypes from './standardEventTypes' 10 | 11 | function fromHammerEvent(eventType, api, callback) { 12 | return api.observable.flatMapLatest( 13 | element => { 14 | if (!element) { 15 | return Observable.empty() 16 | } 17 | const k = api.namespace.join(``) 18 | return fromEvent({element, eventType, k, callback}) 19 | } 20 | ).share() 21 | } 22 | 23 | function makeEventsSelector(api, originalEvents) { 24 | return function events(eventType, ...rest) { 25 | return eventTypes[eventType] ? 26 | originalEvents(eventType, ...rest) : 27 | fromHammerEvent(eventType, api, ...rest) 28 | } 29 | } 30 | 31 | function override(obj, methodName, callback) { 32 | obj[methodName] = callback(obj[methodName]) 33 | } 34 | 35 | function selectCallback(originalSelect) { 36 | return function select(...args) { 37 | const api = originalSelect.apply(this, args) 38 | override(api, `select`, selectCallback) 39 | const originalEvents = api.events 40 | api.events = makeEventsSelector(api, originalEvents) 41 | return api 42 | } 43 | } 44 | 45 | function hammerDriver(domDriverFunc, vTree$) { 46 | if (typeof domDriverFunc !== `function`) { 47 | throw new Error( 48 | `First argument given to hammerDriver() must be a DOM driver function.` 49 | ) 50 | } 51 | if (!vTree$ || typeof vTree$.subscribe !== `function`) { 52 | throw new Error( 53 | `The Hammer Driver expects a stream of virtual DOM elements as input.` 54 | ) 55 | } 56 | 57 | const domDriver = domDriverFunc(vTree$) 58 | override(domDriver, `select`, selectCallback) 59 | 60 | return domDriver 61 | } 62 | 63 | export default hammerDriver 64 | -------------------------------------------------------------------------------- /src/makeHammerDriver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Hammer driver factory 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | import hammerDriver from './hammerDriver' 8 | 9 | function makeHammerDriver(domDriverFunc) { 10 | if (typeof domDriverFunc !== `function`) { 11 | throw new Error( 12 | `First argument given to makeHammerDriver() ` + 13 | `must be a DOM driver function.` 14 | ) 15 | } 16 | return hammerDriver.bind({}, domDriverFunc) 17 | } 18 | 19 | export default makeHammerDriver 20 | -------------------------------------------------------------------------------- /src/standardEventTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Standard Event Types 3 | * @author Frederik Krautwald 4 | * @copyright 2015 Cyclic Materials. All rights reserved. 5 | */ 6 | 7 | // https://developer.mozilla.org/en-US/docs/Web/Events 8 | const standardEventTypes = { 9 | abort: true, 10 | afterprint: true, 11 | animationend: true, 12 | animationiteration: true, 13 | animationstart: true, 14 | audioprocess: true, 15 | audioend: true, 16 | audiostart: true, 17 | beforeprint: true, 18 | beforeunload: true, 19 | beginEvent: true, 20 | blocked: true, 21 | blur: true, 22 | cached: true, 23 | canplay: true, 24 | canplaythrough: true, 25 | change: true, 26 | chargingchange: true, 27 | chargingtimechange: true, 28 | checking: true, 29 | click: true, 30 | close: true, 31 | complete: true, 32 | compositionend: true, 33 | compositionstart: true, 34 | compositionupdate: true, 35 | contextmenu: true, 36 | copy: true, 37 | cut: true, 38 | dblclick: true, 39 | devicelight: true, 40 | devicemotion: true, 41 | deviceorientation: true, 42 | deviceproximity: true, 43 | dischargingtimechange: true, 44 | DOMActivate: true, 45 | DOMAttributeNameChanged: true, 46 | DOMAttrModified: true, 47 | DOMCharacterDataModified: true, 48 | DOMContentLoaded: true, 49 | DOMElementNameChanged: true, 50 | DOMFocusIn: true, 51 | DOMFocusOut: true, 52 | DOMNodeInserted: true, 53 | DOMNodeInsertedIntoDocument: true, 54 | DOMNodeRemoved: true, 55 | DOMNodeRemovedFromDocument: true, 56 | DOMSubtreeModified: true, 57 | downloading: true, 58 | drag: true, 59 | dragend: true, 60 | dragenter: true, 61 | dragleave: true, 62 | dragover: true, 63 | dragstart: true, 64 | drop: true, 65 | durationchange: true, 66 | emptied: true, 67 | end: true, 68 | ended: true, 69 | endEvent: true, 70 | focus: true, 71 | focusin: true, 72 | focusout: true, 73 | fullscreenchange: true, 74 | fullscreenerror: true, 75 | gamepadconnected: true, 76 | gamepaddisconnected: true, 77 | gotpointercapture: true, 78 | hashchange: true, 79 | lostpointercapture: true, 80 | input: true, 81 | invalid: true, 82 | keydown: true, 83 | keypress: true, 84 | keyup: true, 85 | languagechange: true, 86 | levelchange: true, 87 | load: true, 88 | loadeddata: true, 89 | loadedmetadata: true, 90 | loadend: true, 91 | loadstart: true, 92 | message: true, 93 | mousedown: true, 94 | mouseenter: true, 95 | mouseleave: true, 96 | mousemove: true, 97 | mouseout: true, 98 | mouseover: true, 99 | mouseup: true, 100 | nomatch: true, 101 | notificationclick: true, 102 | noupdate: true, 103 | obsolete: true, 104 | offline: true, 105 | online: true, 106 | open: true, 107 | orientationchange: true, 108 | pagehide: true, 109 | pageshow: true, 110 | paste: true, 111 | pause: true, 112 | pointercancel: true, 113 | pointerdown: true, 114 | pointerenter: true, 115 | pointerleave: true, 116 | pointerlockchange: true, 117 | pointerlockerror: true, 118 | pointermove: true, 119 | pointerout: true, 120 | pointerover: true, 121 | pointerup: true, 122 | play: true, 123 | playing: true, 124 | popstate: true, 125 | progress: true, 126 | push: true, 127 | pushsubscriptionchange: true, 128 | ratechange: true, 129 | readystatechange: true, 130 | repeatEvent: true, 131 | reset: true, 132 | resize: true, 133 | result: true, 134 | scroll: true, 135 | seeked: true, 136 | seeking: true, 137 | select: true, 138 | selectstart: true, 139 | selectionchange: true, 140 | show: true, 141 | soundend: true, 142 | soundstart: true, 143 | speechend: true, 144 | speechstart: true, 145 | stalled: true, 146 | start: true, 147 | storage: true, 148 | submit: true, 149 | success: true, 150 | suspend: true, 151 | SVGAbort: true, 152 | SVGError: true, 153 | SVGLoad: true, 154 | SVGResize: true, 155 | SVGScroll: true, 156 | SVGUnload: true, 157 | SVGZoom: true, 158 | timeout: true, 159 | timeupdate: true, 160 | touchcancel: true, 161 | touchend: true, 162 | touchenter: true, 163 | touchleave: true, 164 | touchmove: true, 165 | touchstart: true, 166 | transitionend: true, 167 | unload: true, 168 | updateready: true, 169 | upgradeneeded: true, 170 | userproximity: true, 171 | versionchange: true, 172 | visibilitychange: true, 173 | volumechange: true, 174 | waiting: true, 175 | wheel: true, 176 | } 177 | 178 | //const hammerEventTable = { 179 | // pan: true, 180 | // panstart: true, 181 | // panmove: true, 182 | // panend: true, 183 | // pancancel: true, 184 | // panleft: true, 185 | // panright: true, 186 | // panup: true, 187 | // pandown: true, 188 | // pinch: true, 189 | // pinchstart: true, 190 | // pinchmove: true, 191 | // pinchend: true, 192 | // pinchcancel: true, 193 | // pinchin: true, 194 | // pinchout: true, 195 | // press: true, 196 | // pressup: true, 197 | // rotate: true, 198 | // rotatestart: true, 199 | // rotatemove: true, 200 | // rotateend: true, 201 | // rotatecancel: true, 202 | // swipe: true, 203 | // swipeleft: true, 204 | // swiperight: true, 205 | // swipeup: true, 206 | // swipedown: true, 207 | // tap: true, 208 | //} 209 | 210 | export default standardEventTypes 211 | -------------------------------------------------------------------------------- /test/MockObservable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Observable mock 3 | * @author Frederik Krautwald 4 | */ 5 | 6 | const MockObservable = { 7 | subscribe: () => {}, 8 | } 9 | 10 | export default MockObservable 11 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for hammerDriver 3 | * @author Frederik Krautwald 4 | */ 5 | 6 | /* eslint max-nested-callbacks: 0, max-len: 0, no-unused-expressions: 0 */ 7 | /* global describe, it */ 8 | 9 | import chai from 'chai' 10 | import CycleHammer from './../src/cycle-hammer' 11 | import hammerDriver from './../src/hammerDriver' 12 | import mockDomDriver from './mockDomDriver' 13 | import MockObservable from './MockObservable' 14 | 15 | const expect = chai.expect 16 | const {makeHammerDriver} = CycleHammer 17 | 18 | describe(`makeHammerDriver`, () => { 19 | it(`should be a function`, () => { 20 | expect(makeHammerDriver).to.be.a(`function`) 21 | }) 22 | 23 | it(`should throw if first argument is not a function`, () => { 24 | expect(makeHammerDriver.bind(null, `not a function`)).to.throw( 25 | /First argument given to makeHammerDriver\(\) must be a DOM driver function/ 26 | ) 27 | }) 28 | 29 | it(`should accept a DOM driver as input`, () => { 30 | expect(makeHammerDriver.bind(null, mockDomDriver)).to.not.throw(Error) 31 | }) 32 | 33 | it.skip(`should throw if DOM driver function doesn’t support interface`, () => { 34 | let domDriver = () => {} 35 | expect(makeHammerDriver.bind(null, domDriver)).to.throw( 36 | /DOM driver function must have select\(\) method/ 37 | ) 38 | }) 39 | 40 | it(`should bind the DOM driver function to the hammer driver function`, () => { 41 | const boundHammerDriverName = hammerDriver.bind(null, mockDomDriver).name 42 | expect(makeHammerDriver(mockDomDriver).name).to.equal(boundHammerDriverName) 43 | }) 44 | }) 45 | 46 | describe(`hammerDriver`, () => { 47 | it(`should be a function`, () => { 48 | expect(hammerDriver).to.be.a(`function`) 49 | }) 50 | 51 | it(`should throw if first argument is not a function`, () => { 52 | expect(hammerDriver.bind(null, `not a function`)).to.throw( 53 | / given to hammerDriver\(\) must be a DOM driver function/ 54 | ) 55 | }) 56 | 57 | it(`should throw if second argument is not a subscribable stream of `, () => { 58 | expect(hammerDriver.bind(null, mockDomDriver, {})).to.throw( 59 | /The Hammer Driver expects a stream of virtual DOM elements as input/ 60 | ) 61 | }) 62 | 63 | it(`should return DOM driver’s API`, () => { 64 | const vTree$ = MockObservable 65 | const hammerApi = hammerDriver(mockDomDriver, vTree$) 66 | const domDriverApi = mockDomDriver(vTree$) 67 | expect(hammerApi).to.have.all.keys(domDriverApi) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /test/mockDomDriver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview DOM driver mock 3 | * @author Frederik Krautwald 4 | */ 5 | 6 | function makeEvents() { 7 | return () => { 8 | return {} 9 | } 10 | } 11 | 12 | function makeSelect() { 13 | return () => { 14 | return { 15 | select: makeSelect(), 16 | events: makeEvents(), 17 | } 18 | } 19 | } 20 | 21 | function mockDomDriver() { 22 | return { 23 | select: makeSelect(), 24 | } 25 | } 26 | 27 | export default mockDomDriver 28 | -------------------------------------------------------------------------------- /test/page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | --------------------------------------------------------------------------------