(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.nipplejs = f()}})(function(){var define,module,exports; 'use strict'; // Constants var isTouch = !!('ontouchstart' in window); var isPointer = window.PointerEvent ? true : false; var isMSPointer = window.MSPointerEvent ? true : false; var events = { touch: { start: 'touchstart', move: 'touchmove', end: 'touchend' }, mouse: { start: 'mousedown', move: 'mousemove', end: 'mouseup' }, pointer: { start: 'pointerdown', move: 'pointermove', end: 'pointerup' }, MSPointer: { start: 'MSPointerDown', move: 'MSPointerMove', end: 'MSPointerUp' } }; var toBind; var secondBind = {}; if (isPointer) { toBind = events.pointer; } else if (isMSPointer) { toBind = events.MSPointer; } else if (isTouch) { toBind = events.touch; secondBind = events.mouse; } else { toBind = events.mouse; } /////////////////////// /// UTILS /// /////////////////////// var u = {}; u.distance = function (p1, p2) { var dx = p2.x - p1.x; var dy = p2.y - p1.y; return Math.sqrt((dx * dx) + (dy * dy)); }; u.angle = function(p1, p2) { var dx = p2.x - p1.x; var dy = p2.y - p1.y; return u.degrees(Math.atan2(dy, dx)); }; u.findCoord = function(p, d, a) { var b = {x: 0, y: 0}; a = u.radians(a); b.x = p.x - d * Math.cos(a); b.y = p.y - d * Math.sin(a); return b; }; u.radians = function(a) { return a * (Math.PI / 180); }; u.degrees = function(a) { return a * (180 / Math.PI); }; u.bindEvt = function (el, type, handler) { if (el.addEventListener) { el.addEventListener(type, handler, false); } else if (el.attachEvent) { el.attachEvent(type, handler); } }; u.unbindEvt = function (el, type, handler) { if (el.removeEventListener) { el.removeEventListener(type, handler); } else if (el.detachEvent) { el.detachEvent(type, handler); } }; u.trigger = function (el, type, data) { var evt = new CustomEvent(type, data); el.dispatchEvent(evt); }; u.prepareEvent = function (evt) { evt.preventDefault(); return evt.type.match(/^touch/) ? evt.changedTouches : evt; }; u.getScroll = function () { var x = (window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body) .scrollLeft; var y = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body) .scrollTop; return { x: x, y: y }; }; u.applyPosition = function (el, pos) { if (pos.x && pos.y) { el.style.left = pos.x + 'px'; el.style.top = pos.y + 'px'; } else if (pos.top || pos.right || pos.bottom || pos.left) { el.style.top = pos.top; el.style.right = pos.right; el.style.bottom = pos.bottom; el.style.left = pos.left; } }; u.getTransitionStyle = function (property, values, time) { var obj = u.configStylePropertyObject(property); for (var i in obj) { if (obj.hasOwnProperty(i)) { if (typeof values === 'string') { obj[i] = values + ' ' + time; } else { var st = ''; for (var j = 0, max = values.length; j < max; j += 1) { st += values[j] + ' ' + time + ', '; } obj[i] = st.slice(0, -2); } } } return obj; }; u.getVendorStyle = function (property, value) { var obj = u.configStylePropertyObject(property); for (var i in obj) { if (obj.hasOwnProperty(i)) { obj[i] = value; } } return obj; }; u.configStylePropertyObject = function (prop) { var obj = {}; obj[prop] = ''; var vendors = ['webkit', 'Moz', 'o']; vendors.forEach(function (vendor) { obj[vendor + prop.charAt(0).toUpperCase() + prop.slice(1)] = ''; }); return obj; }; u.extend = function (objA, objB) { for (var i in objB) { if (objB.hasOwnProperty(i)) { objA[i] = objB[i]; } } return objA; }; // Overwrite only what's already present u.safeExtend = function (objA, objB) { var obj = {}; for (var i in objA) { if (objA.hasOwnProperty(i) && objB.hasOwnProperty(i)) { obj[i] = objB[i]; } else if (objA.hasOwnProperty(i)) { obj[i] = objA[i]; } } return obj; }; // Map for array or unique item. u.map = function (ar, fn) { if (ar.length) { for (var i = 0, max = ar.length; i < max; i += 1) { fn(ar[i]); } } else { fn(ar); } }; /////////////////////// /// SUPER CLASS /// /////////////////////// function Super () {}; // Basic event system. Super.prototype.on = function (arg, cb) { var self = this; var types = arg.split(/[ ,]+/g); var type; self._handlers_ = self._handlers_ || {}; for (var i = 0; i < types.length; i += 1) { type = types[i]; self._handlers_[type] = self._handlers_[type] || []; self._handlers_[type].push(cb); } return self; }; Super.prototype.off = function (type, cb) { var self = this; self._handlers_ = self._handlers_ || {}; if (type === undefined) { self._handlers_ = {}; } else if (cb === undefined) { self._handlers_[type] = null; } else if (self._handlers_[type] && self._handlers_[type].indexOf(cb) >= 0) { self._handlers_[type].splice(self._handlers_[type].indexOf(cb), 1); } return self; }; Super.prototype.trigger = function (arg, data) { var self = this; var types = arg.split(/[ ,]+/g); var type; self._handlers_ = self._handlers_ || {}; for (var i = 0; i < types.length; i += 1) { type = types[i]; if (self._handlers_[type] && self._handlers_[type].length) { self._handlers_[type].forEach(function (handler) { handler.call(self, { type: type, target: self }, data); }); } } }; // Configuration Super.prototype.config = function (options) { var self = this; self.options = self.defaults || {}; if (options) { self.options = u.safeExtend(self.options, options); } }; // Bind internal events. Super.prototype.bindEvt = function (el, type) { var self = this; self._domHandlers_ = self._domHandlers_ || {}; self._domHandlers_[type] = function () { if (typeof self['on' + type] === 'function') { self['on' + type].apply(self, arguments); } else { console.warn('[WARNING] : Missing "on' + type + '" handler.'); } }; u.bindEvt(el, toBind[type], self._domHandlers_[type]); if (secondBind[type]) { // Support for both touch and mouse at the same time. u.bindEvt(el, secondBind[type], self._domHandlers_[type]); } return self; }; // Unbind dom events. Super.prototype.unbindEvt = function (el, type) { var self = this; self._domHandlers_ = self._domHandlers_ || {}; u.unbindEvt(el, toBind[type], self._domHandlers_[type]); if (secondBind[type]) { // Support for both touch and mouse at the same time. u.unbindEvt(el, secondBind[type], self._domHandlers_[type]); } delete self._domHandlers_[type]; return this; }; /////////////////////// /// THE NIPPLE /// /////////////////////// function Nipple (collection, options) { this.identifier = options.identifier; this.position = options.position; this.frontPosition = options.frontPosition; this.collection = collection; // Defaults this.defaults = { size: 100, threshold: 0.1, color: 'white', fadeTime: 250, dataOnly: false, restOpacity: 0.5, mode: 'dynamic', zone: document.body }; this.config(options); // Overwrites if (this.options.mode === 'dynamic') { this.options.restOpacity = 0; } this.id = Nipple.id; Nipple.id += 1; this.buildEl() .stylize(); // Nipple's API. this.instance = { el: this.ui.el, on: this.on.bind(this), off: this.off.bind(this), show: this.show.bind(this), hide: this.hide.bind(this), add: this.addToDom.bind(this), remove: this.removeFromDom.bind(this), destroy: this.destroy.bind(this), resetDirection: this.resetDirection.bind(this), computeDirection: this.computeDirection.bind(this), trigger: this.trigger.bind(this), position: this.position, frontPosition: this.frontPosition, ui: this.ui, identifier: this.identifier, id: this.id, options: this.options }; return this.instance; }; Nipple.prototype = new Super(); Nipple.constructor = Nipple; Nipple.id = 0; // Build the dom element of the Nipple instance. Nipple.prototype.buildEl = function (options) { this.ui = {}; if (this.options.dataOnly) { return this; } this.ui.el = document.createElement('div'); this.ui.back = document.createElement('div'); this.ui.front = document.createElement('div'); this.ui.el.className = 'nipple collection_' + this.collection.id; this.ui.back.className = 'back'; this.ui.front.className = 'front'; this.ui.el.setAttribute('id', 'nipple_' + this.collection.id + '_' + this.id); this.ui.el.appendChild(this.ui.back); this.ui.el.appendChild(this.ui.front); return this; }; // Apply CSS to the Nipple instance. Nipple.prototype.stylize = function () { if (this.options.dataOnly) { return this; } var animTime = this.options.fadeTime + 'ms'; var borderStyle = u.getVendorStyle('borderRadius', '50%'); var transitStyle = u.getTransitionStyle('transition', 'opacity', animTime); var styles = {}; styles.el = { position: 'absolute', opacity: this.options.restOpacity, display: 'block', 'zIndex': 999 }; styles.back = { position: 'absolute', display: 'block', width: this.options.size + 'px', height: this.options.size + 'px', marginLeft: -this.options.size / 2 + 'px', marginTop: -this.options.size / 2 + 'px', background: this.options.color, 'opacity': '.5' }; styles.front = { width: this.options.size / 2 + 'px', height: this.options.size / 2 + 'px', position: 'absolute', display: 'block', marginLeft: -this.options.size / 4 + 'px', marginTop: -this.options.size / 4 + 'px', background: this.options.color, 'opacity': '.5' }; u.extend(styles.el, transitStyle); u.extend(styles.back, borderStyle); u.extend(styles.front, borderStyle); this.applyStyles(styles); return this; }; Nipple.prototype.applyStyles = function (styles) { // Apply styles for (var i in this.ui) { if (this.ui.hasOwnProperty(i)) { for (var j in styles[i]) { this.ui[i].style[j] = styles[i][j]; } } } return this; }; // Inject the Nipple instance into DOM. Nipple.prototype.addToDom = function () { // We're not adding it if we're dataOnly or already in dom. if (this.options.dataOnly || document.body.contains(this.ui.el)) { return this; } this.options.zone.appendChild(this.ui.el); return this; }; // Remove the Nipple instance from DOM. Nipple.prototype.removeFromDom = function () { if (this.options.dataOnly || !document.body.contains(this.ui.el)) { return this; } this.options.zone.removeChild(this.ui.el); return this; }; // Entirely destroy this nipple Nipple.prototype.destroy = function () { clearTimeout(this.removeTimeout); clearTimeout(this.showTimeout); clearTimeout(this.restTimeout); this.trigger('destroyed', this.instance); this.removeFromDom(); this.off(); }; // Fade in the Nipple instance. Nipple.prototype.show = function (cb) { var self = this; if (self.options.dataOnly) { return self; } clearTimeout(self.removeTimeout); clearTimeout(self.showTimeout); clearTimeout(self.restTimeout); self.addToDom(); self.restCallback(); setTimeout(function () { self.ui.el.style.opacity = 1; }, 0); self.showTimeout = setTimeout(function () { self.trigger('shown', self.instance); if (typeof cb === 'function') { cb.call(this); } }, self.options.fadeTime); return self; }; // Fade out the Nipple instance. Nipple.prototype.hide = function (cb) { var self = this; if (self.options.dataOnly) { return self; } self.ui.el.style.opacity = self.options.restOpacity; clearTimeout(self.removeTimeout); clearTimeout(self.showTimeout); clearTimeout(self.restTimeout); self.removeTimeout = setTimeout( function () { var display = self.options.mode === 'dynamic' ? 'none' : 'block'; self.ui.el.style.display = display; if (typeof cb === 'function') { cb.call(self); } self.trigger('hidden', self.instance); }, self.options.fadeTime ); self.restPosition(); return self; }; Nipple.prototype.restPosition = function (cb) { var self = this; self.frontPosition = { x: 0, y: 0 }; var animTime = self.options.fadeTime + 'ms'; var transitStyle = {}; transitStyle.front = u.getTransitionStyle('transition', ['top', 'left'], animTime); var styles = {front: {}}; styles.front = { left: self.frontPosition.x + 'px', top: self.frontPosition.y + 'px' }; self.applyStyles(transitStyle); self.applyStyles(styles); self.restTimeout = setTimeout( function () { if (typeof cb === 'function') { cb.call(self); } self.restCallback(); }, self.options.fadeTime ); }; Nipple.prototype.restCallback = function () { var self = this; var transitStyle = {}; transitStyle.front = u.getTransitionStyle('transition', 'none', ''); self.applyStyles(transitStyle); self.trigger('rested', self.instance); }; Nipple.prototype.resetDirection = function () { // Fully rebuild the object to let the iteration possible. this.direction = { x: false, y: false, angle: false }; }; Nipple.prototype.computeDirection = function (obj) { var rAngle = obj.angle.radian; var angle45 = Math.PI / 4; var angle90 = Math.PI / 2; var direction, directionX, directionY; // Angular direction // \ UP / // \ / // LEFT RIGHT // / \ // /DOWN \ // if (rAngle > angle45 && rAngle < (angle45 * 3)) { direction = 'up'; } else if (rAngle > -angle45 && rAngle <= angle45) { direction = 'left'; } else if (rAngle > (-angle45 * 3) && rAngle <= -angle45) { direction = 'down'; } else { direction = 'right'; } // Plain direction // UP | // _______ | RIGHT // LEFT | // DOWN | if (rAngle > -angle90 && rAngle < angle90) { directionX = 'left'; } else { directionX = 'right'; } if (rAngle > 0) { directionY = 'up'; } else { directionY = 'down'; } if (obj.force > this.options.threshold) { var oldDirection = {}; for (var i in this.direction) { if (this.direction.hasOwnProperty(i)) { oldDirection[i] = this.direction[i]; } } var same = {}; this.direction = { x: directionX, y: directionY, angle: direction }; obj.direction = this.direction; for (var i in oldDirection) { if (oldDirection[i] === this.direction[i]) { same[i] = true; } } // If all 3 directions are the same, we don't trigger anything. if (same.x && same.y && same.angle) { return obj; } if (!same.x || !same.y) { this.trigger('plain', obj); } if (!same.x) { this.trigger('plain:' + directionX, obj); } if (!same.y) { this.trigger('plain:' + directionY, obj); } if (!same.angle) { this.trigger('dir dir:' + direction, obj); } } return obj; }; /* global Nipple, Super */ /////////////////////////// /// THE COLLECTION /// /////////////////////////// function Collection (manager, options) { var self = this; self.nipples = []; self.idles = []; self.actives = []; self.ids = []; self.pressureIntervals = {}; self.manager = manager; self.id = Collection.id; Collection.id += 1; // Defaults self.defaults = { zone: document.body, multitouch: false, maxNumberOfNipples: 10, mode: 'dynamic', position: {top: 0, left: 0}, catchDistance: 200, size: 100, threshold: 0.1, color: 'white', fadeTime: 250, dataOnly: false, restOpacity: 0.5 }; self.config(options); // Overwrites if (self.options.mode === 'static' || self.options.mode === 'semi') { self.options.multitouch = false; } if (!self.options.multitouch) { self.options.maxNumberOfNipples = 1; } self.updateBox(); self.prepareNipples(); self.bindings(); self.begin(); return self.nipples; } Collection.prototype = new Super(); Collection.constructor = Collection; Collection.id = 0; Collection.prototype.prepareNipples = function () { var self = this; var nips = self.nipples; // Public API Preparation. nips.on = self.on.bind(self); nips.off = self.off.bind(self); nips.options = self.options; nips.destroy = self.destroy.bind(self); nips.ids = self.ids; nips.id = self.id; nips.processOnMove = self.processOnMove.bind(self); nips.processOnEnd = self.processOnEnd.bind(self); nips.get = function (id) { if (id === undefined) { return nips[0]; } for (var i = 0, max = nips.length; i < max; i += 1) { if (nips[i].identifier === id) { return nips[i]; } } return false; }; }; Collection.prototype.bindings = function () { var self = this; // Touch start event. self.bindEvt(self.options.zone, 'start'); // Avoid native touch actions (scroll, zoom etc...) on the zone. self.options.zone.style.touchAction = 'none'; self.options.zone.style.msTouchAction = 'none'; }; Collection.prototype.begin = function () { var self = this; var opts = self.options; // We place our static nipple // if needed. if (opts.mode === 'static') { var nipple = self.createNipple( opts.position, self.manager.getIdentifier() ); // Add it to the dom. nipple.add(); // Store it in idles. self.idles.push(nipple); } }; // Nipple Factory Collection.prototype.createNipple = function (position, identifier) { var self = this; var scroll = u.getScroll(); var toPutOn = {}; var opts = self.options; if (position.x && position.y) { toPutOn = { x: position.x - (scroll.x + self.box.left), y: position.y - (scroll.y + self.box.top) }; } else if ( position.top || position.right || position.bottom || position.left ) { // We need to compute the position X / Y of the joystick. var dumb = document.createElement('DIV'); dumb.style.display = 'hidden'; dumb.style.top = position.top; dumb.style.right = position.right; dumb.style.bottom = position.bottom; dumb.style.left = position.left; dumb.style.position = 'absolute'; opts.zone.appendChild(dumb); var dumbBox = dumb.getBoundingClientRect(); opts.zone.removeChild(dumb); toPutOn = position; position = { x: dumbBox.left + scroll.x, y: dumbBox.top + scroll.y }; } var nipple = new Nipple(self, { color: opts.color, size: opts.size, threshold: opts.threshold, fadeTime: opts.fadeTime, dataOnly: opts.dataOnly, restOpacity: opts.restOpacity, mode: opts.mode, identifier: identifier, position: position, zone: opts.zone, frontPosition: { x: 0, y: 0 } }); if (!opts.dataOnly) { u.applyPosition(nipple.ui.el, toPutOn); u.applyPosition(nipple.ui.front, nipple.frontPosition); } self.nipples.push(nipple); self.trigger('added ' + nipple.identifier + ':added', nipple); self.manager.trigger('added ' + nipple.identifier + ':added', nipple); self.bindNipple(nipple); return nipple; }; Collection.prototype.updateBox = function () { var self = this; self.box = self.options.zone.getBoundingClientRect(); }; Collection.prototype.bindNipple = function (nipple) { var self = this; var type; // Bubble up identified events. var handler = function (evt, data) { // Identify the event type with the nipple's id. type = evt.type + ' ' + data.id + ':' + evt.type; self.trigger(type, data); }; // When it gets destroyed. nipple.on('destroyed', self.onDestroyed.bind(self)); // Other events that will get bubbled up. nipple.on('shown hidden rested dir plain', handler); nipple.on('dir:up dir:right dir:down dir:left', handler); nipple.on('plain:up plain:right plain:down plain:left', handler); }; Collection.prototype.pressureFn = function (touch, nipple, identifier) { var self = this; var previousPressure = 0; clearInterval(self.pressureIntervals[identifier]); // Create an interval that will read the pressure every 100ms self.pressureIntervals[identifier] = setInterval(function () { var pressure = touch.force || touch.pressure || touch.webkitForce || 0; if (pressure !== previousPressure) { nipple.trigger('pressure', pressure); self.trigger('pressure ' + nipple.identifier + ':pressure', pressure); previousPressure = pressure; } }.bind(self), 100); }; Collection.prototype.onstart = function (evt) { var self = this; var opts = self.options; evt = u.prepareEvent(evt); // Update the box position self.updateBox(); var process = function (touch) { // If we can create new nipples // meaning we don't have more active nipples than we should. if (self.actives.length < opts.maxNumberOfNipples) { self.processOnStart(touch); } }; u.map(evt, process); // We ask upstream to bind the document // on 'move' and 'end' self.manager.bindDocument(); return false; }; Collection.prototype.processOnStart = function (evt) { var self = this; var opts = self.options; var indexInIdles; var identifier = self.manager.getIdentifier(evt); var pressure = evt.force || evt.pressure || evt.webkitForce || 0; var position = { x: evt.pageX, y: evt.pageY }; var nipple = self.getOrCreate(identifier, position); // Update its touch identifier nipple.identifier = identifier; var process = function (nip) { // Trigger the start. nip.trigger('start', nip); self.trigger('start ' + nip.id + ':start', nip); nip.show(); if (pressure > 0) { self.pressureFn(evt, nip, nip.identifier); } // Trigger the first move event. self.processOnMove(evt); }; // Transfer it from idles to actives. if ((indexInIdles = self.idles.indexOf(nipple)) >= 0) { self.idles.splice(indexInIdles, 1); } // Store the nipple in the actives array self.actives.push(nipple); self.ids.push(nipple.identifier); if (opts.mode !== 'semi') { process(nipple); } else { // In semi we check the distance of the touch // to decide if we have to reset the nipple var distance = u.distance(position, nipple.position); if (distance <= opts.catchDistance) { process(nipple); } else { nipple.destroy(); self.processOnStart(evt); return; } } return nipple; }; Collection.prototype.getOrCreate = function (identifier, position) { var self = this; var opts = self.options; var nipple; // If we're in static or semi, we might already have an active. if (/(semi|static)/.test(opts.mode)) { // Get the active one. // TODO: Multi-touche for semi and static will start here. // Return the nearest one. nipple = self.idles[0]; if (nipple) { self.idles.splice(0, 1); return nipple; } if (opts.mode === 'semi') { // If we're in semi mode, we need to create one. return self.createNipple(position, identifier); } console.warn('Coudln\'t find the needed nipple.'); return false; } // In dynamic, we create a new one. nipple = self.createNipple(position, identifier); return nipple; }; Collection.prototype.processOnMove = function (evt) { var self = this; var opts = self.options; var identifier = self.manager.getIdentifier(evt); var nipple = self.nipples.get(identifier); if (!nipple) { // This is here just for safety. // It shouldn't happen. console.error('Found zombie joystick with ID ' + identifier); self.manager.removeIdentifier(identifier); return; } nipple.identifier = identifier; var size = nipple.options.size / 2; var pos = { x: evt.pageX, y: evt.pageY }; var dist = u.distance(pos, nipple.position); var angle = u.angle(pos, nipple.position); var rAngle = u.radians(angle); var force = dist / size; // If distance is bigger than nipple's size // we clamp the position. if (dist > size) { dist = size; pos = u.findCoord(nipple.position, dist, angle); } nipple.frontPosition = { x: pos.x - nipple.position.x, y: pos.y - nipple.position.y }; if (!opts.dataOnly) { u.applyPosition(nipple.ui.front, nipple.frontPosition); } // Prepare event's datas. var toSend = { identifier: nipple.identifier, position: pos, force: force, pressure: evt.force || evt.pressure || evt.webkitForce || 0, distance: dist, angle: { radian: rAngle, degree: angle }, instance: nipple }; // Compute the direction's datas. toSend = nipple.computeDirection(toSend); // Offset angles to follow units circle. toSend.angle = { radian: u.radians(180 - angle), degree: 180 - angle }; // Send everything to everyone. nipple.trigger('move', toSend); self.trigger('move ' + nipple.id + ':move', toSend); }; Collection.prototype.processOnEnd = function (evt) { var self = this; var opts = self.options; var identifier = self.manager.getIdentifier(evt); var nipple = self.nipples.get(identifier); var removedIdentifier = self.manager.removeIdentifier(nipple.identifier); if (!nipple) { return; } if (!opts.dataOnly) { nipple.hide(function () { if (opts.mode === 'dynamic') { nipple.trigger('removed', nipple); self.trigger('removed ' + nipple.id + ':removed', nipple); self.manager .trigger('removed ' + nipple.id + ':removed', nipple); nipple.destroy(); } }); } // Clear the pressure interval reader clearInterval(self.pressureIntervals[nipple.identifier]); // Reset the direciton of the nipple, to be able to trigger a new direction // on start. nipple.resetDirection(); nipple.trigger('end', nipple); self.trigger('end ' + nipple.id + ':end', nipple); // Remove identifier from our bank. if (self.ids.indexOf(nipple.identifier) >= 0) { self.ids.splice(self.ids.indexOf(nipple.identifier), 1); } // Clean our actives array. if (self.actives.indexOf(nipple) >= 0) { self.actives.splice(self.actives.indexOf(nipple), 1); } if (/(semi|static)/.test(opts.mode)) { // Transfer nipple from actives to idles // if we're in semi or static mode. self.idles.push(nipple); } else if (self.nipples.indexOf(nipple) >= 0) { // Only if we're not in semi or static mode // we can remove the instance. self.nipples.splice(self.nipples.indexOf(nipple), 1); } // We unbind move and end. self.manager.unbindDocument(); // We add back the identifier of the idle nipple; if (/(semi|static)/.test(opts.mode)) { self.manager.ids[removedIdentifier.id] = removedIdentifier.identifier; } }; // Remove destroyed nipple from the lists Collection.prototype.onDestroyed = function(evt, nipple) { var self = this; if (self.nipples.indexOf(nipple) >= 0) { self.nipples.splice(self.nipples.indexOf(nipple), 1); } if (self.actives.indexOf(nipple) >= 0) { self.actives.splice(self.actives.indexOf(nipple), 1); } if (self.idles.indexOf(nipple) >= 0) { self.idles.splice(self.idles.indexOf(nipple), 1); } if (self.ids.indexOf(nipple.identifier) >= 0) { self.ids.splice(self.ids.indexOf(nipple.identifier), 1); } // Remove the identifier from our bank self.manager.removeIdentifier(nipple.identifier); // We unbind move and end. self.manager.unbindDocument(); }; // Cleanly destroy the manager Collection.prototype.destroy = function () { var self = this; self.unbindEvt(self.options.zone, 'start'); // Destroy nipples. self.nipples.forEach(function(nipple) { nipple.destroy(); }); // Clean 3DTouch intervals. for (var i in self.pressureIntervals) { if (self.pressureIntervals.hasOwnProperty(i)) { clearInterval(self.pressureIntervals[i]); } } // Notify the manager passing the instance self.trigger('destroyed', self.nipples); // We unbind move and end. self.manager.unbindDocument(); // Unbind everything. self.off(); }; /* global u, Super, Collection */ /////////////////////// /// MANAGER /// /////////////////////// function Manager (options) { var self = this; self.ids = {}; self.index = 0; self.collections = []; self.config(options); self.prepareCollections(); // Listen for resize, to reposition every joysticks var resizeTimer; u.bindEvt(window, 'resize', function (evt) { clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { var pos; var scroll = u.getScroll(); self.collections.forEach(function (collection) { collection.forEach(function (nipple) { pos = nipple.el.getBoundingClientRect(); nipple.position = { x: scroll.x + pos.left, y: scroll.y + pos.top }; }); }); }, 100); }); return self.collections; }; Manager.prototype = new Super(); Manager.constructor = Manager; Manager.prototype.prepareCollections = function () { var self = this; // Public API Preparation. self.collections.create = self.create.bind(self); // Listen to anything self.collections.on = self.on.bind(self); // Unbind general events self.collections.off = self.off.bind(self); // Destroy everything self.collections.destroy = self.destroy.bind(self); // Get any nipple self.collections.get = function (id) { var nipple; self.collections.every(function (collection) { if (nipple = collection.get(id)) { return false; } return true; }); return nipple; }; }; Manager.prototype.create = function (options) { return this.createCollection(options); }; // Collection Factory Manager.prototype.createCollection = function (options) { var self = this; var collection = new Collection(self, options); self.bindCollection(collection); self.collections.push(collection); return collection; }; Manager.prototype.bindCollection = function (collection) { var self = this; var type; // Bubble up identified events. var handler = function (evt, data) { // Identify the event type with the nipple's identifier. type = evt.type + ' ' + data.id + ':' + evt.type; self.trigger(type, data); }; // When it gets destroyed we clean. collection.on('destroyed', self.onDestroyed.bind(self)); // Other events that will get bubbled up. collection.on('shown hidden rested dir plain', handler); collection.on('dir:up dir:right dir:down dir:left', handler); collection.on('plain:up plain:right plain:down plain:left', handler); }; Manager.prototype.bindDocument = function () { var self = this; // Bind only if not already binded if (!self.binded) { self.bindEvt(document, 'move') .bindEvt(document, 'end'); self.binded = true; } }; Manager.prototype.unbindDocument = function (force) { var self = this; // If there are no touch left // unbind the document. if (!Object.keys(self.ids).length || force === true) { self.unbindEvt(document, 'move') .unbindEvt(document, 'end'); self.binded = false; } }; Manager.prototype.getIdentifier = function (evt) { var id; // If no event, simple increment if (!evt) { id = this.index; } else { // Extract identifier from event object. // Unavailable in mouse events so replaced by latest increment. id = evt.identifier === undefined ? evt.pointerId : evt.identifier; if (id === undefined) { id = this.latest || 0; } } if (this.ids[id] === undefined) { this.ids[id] = this.index; this.index += 1; } // Keep the latest id used in case we're using an unidentified mouseEvent this.latest = id; return this.ids[id]; }; Manager.prototype.removeIdentifier = function (identifier) { var removed = {}; for (var id in this.ids) { if (this.ids[id] === identifier) { removed.id = id; removed.identifier = this.ids[id]; delete this.ids[id]; break; } } return removed; }; Manager.prototype.onmove = function (evt) { var self = this; self.onAny('move', evt); return false; }; Manager.prototype.onend = function (evt) { var self = this; self.onAny('end', evt); return false; }; Manager.prototype.onAny = function (which, evt) { var self = this; var id; var processFn = 'processOn' + which.charAt(0).toUpperCase() + which.slice(1); evt = u.prepareEvent(evt); var processColl = function (e, id, coll) { if (coll.ids.indexOf(id) >= 0) { coll[processFn](e); // Mark the event to avoid cleaning it later. e._found_ = true; } }; var processEvt = function (e) { id = self.getIdentifier(e); u.map(self.collections, processColl.bind(null, e, id)); // If the event isn't handled by any collection, // we need to clean its identifier. if (!e._found_) { self.removeIdentifier(id); } }; u.map(evt, processEvt); return false; }; // Cleanly destroy the manager Manager.prototype.destroy = function () { var self = this; self.unbindDocument(true); self.ids = {}; self.index = 0; self.collections.forEach(function(collection) { collection.destroy(); }); self.off(); }; // When a collection gets destroyed // we clean behind. Manager.prototype.onDestroyed = function (evt, coll) { var self = this; if (self.collections.indexOf(coll) < 0) { return false; } self.collections.splice(self.collections.indexOf(coll), 1); }; var factory = new Manager(); return { create: function (options) { return factory.create(options); }, factory: factory }; });