├── .meteor ├── .gitignore ├── release └── packages ├── packages └── .gitignore ├── smart.json ├── client ├── home.html ├── routes.js ├── views │ ├── pad.js │ └── pad.html └── hammer.js ├── README.md ├── streams.js ├── smart.lock ├── blackboard.css └── lib ├── remote_user.js └── pad.js /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | 0.6.4 2 | -------------------------------------------------------------------------------- /packages/.gitignore: -------------------------------------------------------------------------------- 1 | streams 2 | cluster 3 | router 4 | page-js-ie-support 5 | -------------------------------------------------------------------------------- /smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | "streams": {}, 4 | "cluster": {}, 5 | "router": {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/home.html: -------------------------------------------------------------------------------- 1 | 2 | Realtime Blackboard with Meteor Streams 3 | 4 | 5 | 6 | {{renderPage}} 7 | 8 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | preserve-inputs 7 | bootstrap 8 | madewith 9 | -------------------------------------------------------------------------------- /client/routes.js: -------------------------------------------------------------------------------- 1 | Meteor.Router.add({ 2 | '/': function() { 3 | var newPadId = Random.id(); 4 | location.href = '/' + newPadId; 5 | }, 6 | 7 | '/:id': { 8 | as: 'pad', 9 | to: function(id) { 10 | Session.set('padId', id); 11 | return 'pad' 12 | } 13 | } 14 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | streams-blackboard 2 | ================== 3 | 4 | Realtime Blackboard with [Meteor Streams](http://meteorhacks.com/introducing-meteor-streams.html) 5 | 6 | Live App: 7 | 8 | ### Learn How to create this app - Click below to get your free copy 9 | 10 | [![Stream Blackboard Live App](http://i.imgur.com/uzKa2CM.png)](http://meteorhacks.com/realtime-blackboard.html) 11 | -------------------------------------------------------------------------------- /client/views/pad.js: -------------------------------------------------------------------------------- 1 | var pad; 2 | var remoteUser; 3 | 4 | Meteor.startup(function() { 5 | Deps.autorun(function() { 6 | if(pad) { 7 | pad.close(); 8 | remoteUser.close(); 9 | } 10 | 11 | var padId = Session.get('padId'); 12 | pad = new Pad(padId); 13 | remoteUser = new RemoteUser(padId, pad); 14 | }); 15 | }); 16 | 17 | $(function() { 18 | $('body').on('click', '#wipe', function() { 19 | pad.wipe(true); 20 | }); 21 | 22 | $('body').on('click', '#set-nickname', function() { 23 | var name = prompt('Enter your nickname'); 24 | if(name && name.trim() != '') { 25 | pad.setNickname(name); 26 | } 27 | }); 28 | 29 | $('body').on('click', '#create-new', function() { 30 | var newPadId = Random.id(); 31 | Meteor.Router.to('pad', newPadId); 32 | }); 33 | }); -------------------------------------------------------------------------------- /streams.js: -------------------------------------------------------------------------------- 1 | LineStream = new Meteor.Stream('lines'); 2 | 3 | if(Meteor.isServer) { 4 | var subscriptionPads = {}; 5 | LineStream.on('pad', function(padId) { 6 | var subscriptionId = this.subscriptionId; 7 | subscriptionPads[subscriptionId] = padId; 8 | 9 | this.onDisconnect = function() { 10 | subscriptionPads[subscriptionId] = undefined; 11 | }; 12 | }); 13 | 14 | LineStream.permissions.read(function(event) { 15 | var matched = event.match(/(.*):/); 16 | if(matched) { 17 | var padId = matched[1]; 18 | return subscriptionPads[this.subscriptionId] == padId; 19 | } else { 20 | //only allows events with padId to read from the stream 21 | return false; 22 | } 23 | }, false); 24 | 25 | LineStream.permissions.write(function(event) { 26 | return true; 27 | }); 28 | } -------------------------------------------------------------------------------- /smart.lock: -------------------------------------------------------------------------------- 1 | { 2 | "meteor": {}, 3 | "dependencies": { 4 | "basePackages": { 5 | "streams": {}, 6 | "cluster": {}, 7 | "router": {} 8 | }, 9 | "packages": { 10 | "streams": { 11 | "git": "https://github.com/arunoda/meteor-streams.git", 12 | "tag": "v0.1.12", 13 | "commit": "d587ad9f5babbc267f026f41362f235809c931fc" 14 | }, 15 | "cluster": { 16 | "git": "https://github.com/arunoda/meteor-cluster.git", 17 | "tag": "v0.1.6", 18 | "commit": "74e5b017c2c1eea7bf02eae6a4f3dd6b9556b3be" 19 | }, 20 | "router": { 21 | "git": "https://github.com/tmeasday/meteor-router.git", 22 | "tag": "v0.5.1", 23 | "commit": "32e377c7703bb119acccc859fc7296882903cbe7" 24 | }, 25 | "page-js-ie-support": { 26 | "git": "https://github.com/tmeasday/meteor-page-js-ie-support.git", 27 | "tag": "v1.3.5", 28 | "commit": "b99ed8380aefd10b2afc8f18d9eed4dd0d8ea9cb" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/views/pad.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blackboard.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | overflow: hidden; 4 | } 5 | 6 | #banner { 7 | text-align: center; 8 | padding: 5px 0px 0px 0px; 9 | color: white; 10 | font-size: 18px; 11 | margin: 0px 0px 10px 0px; 12 | } 13 | 14 | #banner a { 15 | color: inherit; 16 | text-decoration: none; 17 | border-bottom: 1px dashed white; 18 | } 19 | 20 | #header { 21 | position: fixed; 22 | top: 0px; 23 | left: 0px; 24 | color: rgb(220, 220, 220); 25 | border-bottom: 1px solid rgb(100, 100, 100); 26 | padding: 15px 5px 5px 10px; 27 | width: 100%; 28 | } 29 | 30 | #header #heading { 31 | float: left; 32 | width: 300px; 33 | } 34 | 35 | #header #controls { 36 | float: right; 37 | width: 600px; 38 | text-align: right; 39 | padding-right: 20px; 40 | } 41 | 42 | #header #heading h1 { 43 | font-size: 28px; 44 | line-height: 25px; 45 | margin: 0px 0px 8px 0px; 46 | float: left; 47 | } 48 | 49 | #header #heading h2 { 50 | margin-bottom: 10px; 51 | font-size: 16px; 52 | line-height: 16px; 53 | font-weight: normal; 54 | } 55 | 56 | #header #heading h2 a { 57 | color: inherit; 58 | text-decoration: none; 59 | font-weight: bold; 60 | border-bottom: 1px dashed white; 61 | } 62 | 63 | .nickname { 64 | position: absolute; 65 | font-family: 'Arial'; 66 | padding: 1px 4px 1px 4px; 67 | font-size: 13px; 68 | border-radius: 3px; 69 | border: 2px solid rgb(100, 100, 100); 70 | color: white; 71 | background-color: black; 72 | } -------------------------------------------------------------------------------- /lib/remote_user.js: -------------------------------------------------------------------------------- 1 | if(!Meteor.isClient) return; 2 | 3 | this.RemoteUser = function RemoteUser(padId, pad) { 4 | var users = {}; 5 | 6 | LineStream.on(padId + ':dragstart', function(nickname, position, color) { 7 | var pointer = $($('#tmpl-nickname').text()); 8 | pointer.text(nickname); 9 | positionPointer(pointer, position); 10 | 11 | $('body').append(pointer); 12 | 13 | users[nickname] = { 14 | color: color, 15 | from: position, 16 | pointer: pointer 17 | }; 18 | }); 19 | 20 | LineStream.on(padId + ':dragend', function(nickname) { 21 | var user = users[nickname]; 22 | if(user) { 23 | user.pointer.remove(); 24 | users[nickname] = undefined; 25 | } 26 | }); 27 | 28 | LineStream.on(padId + ':drag', function(nickname, to) { 29 | var user = users[nickname]; 30 | if(user) { 31 | pad.drawLine(user.from, to, user.color); 32 | positionPointer(user.pointer, to); 33 | user.from = to; 34 | } 35 | }); 36 | 37 | LineStream.on(padId + ':wipe', function(nickname) { 38 | pad.wipe(); 39 | }); 40 | 41 | function positionPointer(pointer, position) { 42 | pointer.css({ 43 | top: position.y + 10, 44 | left: position.x + 10 45 | }); 46 | } 47 | 48 | this.close = function() { 49 | LineStream.removeAllListeners(padId + ':dragstart'); 50 | LineStream.removeAllListeners(padId + ':dragend'); 51 | LineStream.removeAllListeners(padId + ':drag'); 52 | LineStream.removeAllListeners(padId + ':wipe'); 53 | }; 54 | } -------------------------------------------------------------------------------- /lib/pad.js: -------------------------------------------------------------------------------- 1 | if(!Meteor.isClient) return; 2 | 3 | this.Pad = function Pad(id) { 4 | var canvas = $('canvas'); 5 | var ctx = canvas[0].getContext('2d'); 6 | var drawing = false; 7 | var from; 8 | var skipCount = 0; 9 | var nickname; 10 | var color; 11 | 12 | setNickname(localStorage.getItem('nickname') || Random.id()); 13 | 14 | //send padid to the sever 15 | LineStream.emit('pad', id); 16 | 17 | var pad = canvas.attr({ 18 | width: $(window).width(), 19 | height: $(window).height() 20 | }).hammer() 21 | 22 | pad.on('dragstart', onDragStart); 23 | pad.on('dragend', onDragEnd); 24 | pad.on('drag', onDrag); 25 | 26 | function onDrag(event) { 27 | if(drawing) { 28 | var to = getPosition(event); 29 | drawLine(from, to, color); 30 | LineStream.emit(id + ':drag', nickname, to); 31 | from = to; 32 | skipCount = 0; 33 | } 34 | } 35 | 36 | function onDragStart(event) { 37 | drawing = true; 38 | from = getPosition(event); 39 | LineStream.emit(id + ':dragstart', nickname, from, color); 40 | } 41 | 42 | function onDragEnd() { 43 | drawing = false; 44 | LineStream.emit(id + ':dragend', nickname); 45 | } 46 | 47 | function getPosition(event) { 48 | return {x: parseInt(event.gesture.center.pageX), y: parseInt(event.gesture.center.pageY)}; 49 | } 50 | 51 | function drawLine(from, to, color) { 52 | ctx.strokeStyle = color; 53 | ctx.beginPath(); 54 | ctx.moveTo(from.x, from.y); 55 | ctx.lineTo(to.x, to.y); 56 | ctx.closePath(); 57 | ctx.stroke(); 58 | } 59 | 60 | function setNickname(name) { 61 | nickname = name; 62 | $('#show-nickname b').text(nickname); 63 | localStorage.setItem('nickname', nickname); 64 | 65 | color = localStorage.getItem('color-' + nickname); 66 | if(!color) { 67 | color = getRandomColor(); 68 | localStorage.setItem('color-' + nickname, color); 69 | } 70 | } 71 | 72 | function wipe(emitAlso) { 73 | ctx.fillRect(0, 0, canvas.width(), canvas.height()); 74 | if(emitAlso) { 75 | LineStream.emit(id + ':wipe', nickname); 76 | } 77 | } 78 | 79 | ctx.strokeStyle = color; 80 | ctx.fillStyle = '#000000'; 81 | ctx.lineCap = 'round'; 82 | ctx.lineWidth = 3; 83 | 84 | ctx.fillRect(0, 0, canvas.width(), canvas.height()); 85 | 86 | // Stop iOS from doing the bounce thing with the screen 87 | document.ontouchmove = function(event){ 88 | event.preventDefault(); 89 | } 90 | 91 | //expose API 92 | this.drawLine = drawLine; 93 | this.wipe = wipe; 94 | this.setNickname = setNickname; 95 | this.close = function() { 96 | pad.off('dragstart', onDragStart); 97 | pad.off('dragend', onDragEnd); 98 | pad.off('drag', onDrag); 99 | }; 100 | } 101 | 102 | 103 | function getRandomColor() { 104 | var letters = '0123456789ABCDEF'.split(''); 105 | var color = '#'; 106 | for (var i = 0; i < 6; i++ ) { 107 | color += letters[Math.round(Math.random() * 15)]; 108 | } 109 | return color; 110 | } 111 | -------------------------------------------------------------------------------- /client/hammer.js: -------------------------------------------------------------------------------- 1 | /*! Hammer.JS - v1.0.2 - 2013-02-27 2 | * http://eightmedia.github.com/hammer.js 3 | * 4 | * Copyright (c) 2013 Jorik Tangelder ; 5 | * Licensed under the MIT license */ 6 | 7 | (function(t){"use strict";function e(){if(!n.READY){n.event.determineEventTypes();for(var t in n.gestures)n.gestures.hasOwnProperty(t)&&n.detection.register(n.gestures[t]);n.event.onTouch(document,n.EVENT_MOVE,n.detection.detect),n.event.onTouch(document,n.EVENT_END,n.detection.endDetect),n.READY=!0}}var n=function(t,e){return new n.Instance(t,e||{})};n.defaults={stop_browser_behavior:{userSelect:"none",touchCallout:"none",touchAction:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},n.HAS_POINTEREVENTS=navigator.msPointerEnabled,n.HAS_TOUCHEVENTS="ontouchstart"in t,n.EVENT_TYPES={},n.DIRECTION_DOWN="down",n.DIRECTION_LEFT="left",n.DIRECTION_UP="up",n.DIRECTION_RIGHT="right",n.POINTER_MOUSE="mouse",n.POINTER_TOUCH="touch",n.EVENT_START="start",n.EVENT_MOVE="move",n.EVENT_END="end";var i=navigator.userAgent;n.STOP_MOUSEEVENTS=n.HAS_TOUCHEVENTS&&i.match(/(like mac os x.*mobile.*safari)|android|blackberry/i),n.plugins={},n.READY=!1,n.Instance=function(t,i){var r=this;return e(),this.element=t,this.enabled=!0,this.options=n.utils.extend(n.utils.extend({},n.defaults),i||{}),this.options.stop_browser_behavior&&n.utils.stopDefaultBrowserBehavior(this),n.event.onTouch(t,n.EVENT_START,function(t){r.enabled&&n.detection.startDetect(r,t)}),this},n.Instance.prototype={on:function(t,e){for(var n=t.split(" "),i=0;n.length>i;i++)this.element.addEventListener(n[i],e,!1);return this},off:function(t,e){for(var n=t.split(" "),i=0;n.length>i;i++)this.element.removeEventListener(n[i],e,!1);return this},trigger:function(t,e){var n=document.createEvent("Event");return n.initEvent(t,!0,!0),n.gesture=e,this.element.dispatchEvent(n),this},enable:function(t){return this.enabled=t,this}};var r=null,s=!1,o=!1;n.event={bindDom:function(t,e,n){for(var i=e.split(" "),r=0;i.length>r;r++)t.addEventListener(i[r],n,!1)},onTouch:function(t,e,i){var a=this;this.bindDom(t,n.EVENT_TYPES[e],function(c){var u=c.type.toLowerCase();u.match(/mouse/)&&n.STOP_MOUSEEVENTS||(u.match(/start|down|move/)&&(1===c.which||u.match(/touch/)||c.pointerType&&c.pointerType==c.MSPOINTER_TYPE_TOUCH)&&(s=!0),u.match(/touch|pointer/)&&(o=!0),!s||o&&u.match(/mouse/)||(n.HAS_POINTEREVENTS&&e!=n.EVENT_END&&n.PointerEvent.updatePointer(e,c),e===n.EVENT_END&&null!==r?c=r:r=c,i.call(n.detection,a.collectEventData(t,e,c)),n.HAS_POINTEREVENTS&&e==n.EVENT_END&&n.PointerEvent.updatePointer(e,c)),u.match(/up|cancel|end/)&&(s=!1,o=!1,r=null,n.PointerEvent.reset()))})},determineEventTypes:function(){var t;t=n.HAS_POINTEREVENTS?["MSPointerDown","MSPointerMove","MSPointerUp MSPointerCancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],n.EVENT_TYPES[n.EVENT_START]=t[0],n.EVENT_TYPES[n.EVENT_MOVE]=t[1],n.EVENT_TYPES[n.EVENT_END]=t[2]},getTouchList:function(t){return n.HAS_POINTEREVENTS?n.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var r=this.getTouchList(i,e),s=n.POINTER_TOUCH;return(i.type.match(/mouse/)||i.poinerType&&i.pointerType===i.MSPOINTER_TYPE_MOUSE)&&(s=n.POINTER_MOUSE),{center:n.utils.getCenter(r),timestamp:i.timestamp||(new Date).getTime(),target:i.target,touches:r,eventType:e,pointerType:s,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return n.detection.stopDetect()}}}},n.PointerEvent={pointers:{},getTouchList:function(){var t=this.pointers,e=[];return Object.keys(t).sort().forEach(function(n){e.push(t[n])}),e},updatePointer:function(t,e){t==n.EVENT_END?delete this.pointers[e.pointerId]:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e)},reset:function(){this.pointers={}}},n.utils={extend:function(t,e){for(var n in e)t[n]=e[n];return t},getCenter:function(t){for(var e=[],n=[],i=0,r=t.length;r>i;i++)e.push(t[i].pageX),n.push(t[i].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,n)+Math.max.apply(Math,n))/2}},getVelocity:function(t,e,n){return{x:Math.abs(e/t)||0,y:Math.abs(n/t)||0}},getAngle:function(t,e){var n=e.pageY-t.pageY,i=e.pageX-t.pageX;return 180*Math.atan2(n,i)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),r=Math.abs(t.pageY-e.pageY);return i>=r?t.pageX-e.pageX>0?n.DIRECTION_LEFT:n.DIRECTION_RIGHT:t.pageY-e.pageY>0?n.DIRECTION_UP:n.DIRECTION_DOWN},getDistance:function(t,e){var n=e.pageX-t.pageX,i=e.pageY-t.pageY;return Math.sqrt(n*n+i*i)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==n.DIRECTION_UP||t==n.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t){var e,n=["webkit","khtml","moz","ms","o",""],i=t.options.stop_browser_behavior,r=t.element;if(i&&r.style){for(var s=0;n.length>s;s++)for(var o in i)i.hasOwnProperty(o)&&(e=o,n[s]&&(e=n[s]+e.substring(0,1).toUpperCase()+e.substring(1)),r.style[e]=i[o]);"none"==i.userSelect&&(r.onselectstart=function(){return!1})}}},n.detection={gestures:[],current:null,previous:null,stopped:!1,startDetect:function(t,e){this.current||(this.stopped=!1,this.current={inst:t,startEvent:n.utils.extend({},e),lastEvent:!1,name:""},this.detect(e))},detect:function(t){if(this.current&&!this.stopped){t=this.extendEventData(t);for(var e=this.current.inst.options,n=0,i=this.gestures.length;i>n;n++){var r=this.gestures[n];if(!this.stopped&&e[r.name]!==!1&&r.handler.call(r,t,this.current.inst)===!1){this.stopDetect();break}}this.current&&(this.current.lastEvent=t)}},endDetect:function(t){this.detect(t),this.stopDetect()},stopDetect:function(){this.previous=n.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,r=t.touches.length;r>i;i++)e.touches.push(n.utils.extend({},t.touches[i]))}var s=t.timestamp-e.timestamp,o=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,c=n.utils.getVelocity(s,o,a);return n.utils.extend(t,{deltaTime:s,deltaX:o,deltaY:a,velocityX:c.x,velocityY:c.y,distance:n.utils.getDistance(e.center,t.center),angle:n.utils.getAngle(e.center,t.center),direction:n.utils.getDirection(e.center,t.center),scale:n.utils.getScale(e.touches,t.touches),rotation:n.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===void 0&&(e[t.name]=!0),n.utils.extend(n.defaults,e),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},n.gestures=n.gestures||{},n.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case n.EVENT_START:clearTimeout(this.timer),n.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==n.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case n.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case n.EVENT_END:clearTimeout(this.timer)}}},n.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==n.EVENT_END){var i=n.detection.previous;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;n.detection.current.name=i&&"tap"==i.name&&t.timestamp-i.lastEvent.timestamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},n.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1},triggered:!1,handler:function(t,e){if(n.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),this.triggered=!1,void 0;if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case n.EVENT_START:this.triggered=!1;break;case n.EVENT_MOVE:if(t.distancet.deltaY?n.DIRECTION_UP:n.DIRECTION_DOWN:0>t.deltaX?n.DIRECTION_LEFT:n.DIRECTION_RIGHT),this.triggered||(e.trigger(this.name+"start",t),this.triggered=!0),e.trigger(this.name,t),e.trigger(this.name+t.direction,t),(e.options.drag_block_vertical&&n.utils.isVertical(t.direction)||e.options.drag_block_horizontal&&!n.utils.isVertical(t.direction))&&t.preventDefault();break;case n.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},n.gestures.Transform={name:"transform",index:45,defaults:{transform_min_scale:.01,transform_min_rotation:1,transform_always_block:!1},triggered:!1,handler:function(t,e){if(n.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),this.triggered=!1,void 0;if(!(2>t.touches.length))switch(e.options.transform_always_block&&t.preventDefault(),t.eventType){case n.EVENT_START:this.triggered=!1;break;case n.EVENT_MOVE:var i=Math.abs(1-t.scale),r=Math.abs(t.rotation);if(e.options.transform_min_scale>i&&e.options.transform_min_rotation>r)return;n.detection.current.name=this.name,this.triggered||(e.trigger(this.name+"start",t),this.triggered=!0),e.trigger(this.name,t),r>e.options.transform_min_rotation&&e.trigger("rotate",t),i>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(1>t.scale?"in":"out"),t));break;case n.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},n.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1},handler:function(t,e){e.options.prevent_default&&t.preventDefault(),t.eventType==n.EVENT_START&&e.trigger(this.name,t)}},n.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==n.EVENT_END&&e.trigger(this.name,t)}},t.Hammer=n,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return n})})(window),function(t){Hammer.event.bindDom=function(e,n,i){t(e).on(n,function(t){var e=t.originalEvent;e.pageX||(e.pageX=t.pageX,e.pageY=t.pageY),e.target||(e.target=t.target),e.button&&(e.which=e.button),e.preventDefault||(e.preventDefault=t.preventDefault),e.stopPropagation||(e.stopPropagation=t.stopPropagation),i.call(this,e)})},Hammer.Instance.prototype.on=function(e,n){return t(this.element).on(e,n)},Hammer.Instance.prototype.off=function(e,n){return t(this.element).off(e,n)},Hammer.Instance.prototype.trigger=function(e,n){return t(n.srcEvent.target).trigger({type:e,gesture:n})},t.fn.hammer=function(e){return this.each(function(){var n=t(this),i=n.data("hammer");i?i&&e&&Hammer.utils.extend(i.options,e):n.data("hammer",Hammer(this,e||{}))})}}(jQuery); --------------------------------------------------------------------------------