nipplejs.js 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412
  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.nipplejs = f()}})(function(){var define,module,exports;
  2. 'use strict';
  3. // Constants
  4. var isTouch = !!('ontouchstart' in window);
  5. var isPointer = window.PointerEvent ? true : false;
  6. var isMSPointer = window.MSPointerEvent ? true : false;
  7. var events = {
  8. touch: {
  9. start: 'touchstart',
  10. move: 'touchmove',
  11. end: 'touchend'
  12. },
  13. mouse: {
  14. start: 'mousedown',
  15. move: 'mousemove',
  16. end: 'mouseup'
  17. },
  18. pointer: {
  19. start: 'pointerdown',
  20. move: 'pointermove',
  21. end: 'pointerup'
  22. },
  23. MSPointer: {
  24. start: 'MSPointerDown',
  25. move: 'MSPointerMove',
  26. end: 'MSPointerUp'
  27. }
  28. };
  29. var toBind;
  30. var secondBind = {};
  31. if (isPointer) {
  32. toBind = events.pointer;
  33. } else if (isMSPointer) {
  34. toBind = events.MSPointer;
  35. } else if (isTouch) {
  36. toBind = events.touch;
  37. secondBind = events.mouse;
  38. } else {
  39. toBind = events.mouse;
  40. }
  41. ///////////////////////
  42. /// UTILS ///
  43. ///////////////////////
  44. var u = {};
  45. u.distance = function (p1, p2) {
  46. var dx = p2.x - p1.x;
  47. var dy = p2.y - p1.y;
  48. return Math.sqrt((dx * dx) + (dy * dy));
  49. };
  50. u.angle = function(p1, p2) {
  51. var dx = p2.x - p1.x;
  52. var dy = p2.y - p1.y;
  53. return u.degrees(Math.atan2(dy, dx));
  54. };
  55. u.findCoord = function(p, d, a) {
  56. var b = {x: 0, y: 0};
  57. a = u.radians(a);
  58. b.x = p.x - d * Math.cos(a);
  59. b.y = p.y - d * Math.sin(a);
  60. return b;
  61. };
  62. u.radians = function(a) {
  63. return a * (Math.PI / 180);
  64. };
  65. u.degrees = function(a) {
  66. return a * (180 / Math.PI);
  67. };
  68. u.bindEvt = function (el, type, handler) {
  69. if (el.addEventListener) {
  70. el.addEventListener(type, handler, false);
  71. } else if (el.attachEvent) {
  72. el.attachEvent(type, handler);
  73. }
  74. };
  75. u.unbindEvt = function (el, type, handler) {
  76. if (el.removeEventListener) {
  77. el.removeEventListener(type, handler);
  78. } else if (el.detachEvent) {
  79. el.detachEvent(type, handler);
  80. }
  81. };
  82. u.trigger = function (el, type, data) {
  83. var evt = new CustomEvent(type, data);
  84. el.dispatchEvent(evt);
  85. };
  86. u.prepareEvent = function (evt) {
  87. evt.preventDefault();
  88. return evt.type.match(/^touch/) ? evt.changedTouches : evt;
  89. };
  90. u.getScroll = function () {
  91. var x = (window.pageXOffset !== undefined) ?
  92. window.pageXOffset :
  93. (document.documentElement || document.body.parentNode || document.body)
  94. .scrollLeft;
  95. var y = (window.pageYOffset !== undefined) ?
  96. window.pageYOffset :
  97. (document.documentElement || document.body.parentNode || document.body)
  98. .scrollTop;
  99. return {
  100. x: x,
  101. y: y
  102. };
  103. };
  104. u.applyPosition = function (el, pos) {
  105. if (pos.x && pos.y) {
  106. el.style.left = pos.x + 'px';
  107. el.style.top = pos.y + 'px';
  108. } else if (pos.top || pos.right || pos.bottom || pos.left) {
  109. el.style.top = pos.top;
  110. el.style.right = pos.right;
  111. el.style.bottom = pos.bottom;
  112. el.style.left = pos.left;
  113. }
  114. };
  115. u.getTransitionStyle = function (property, values, time) {
  116. var obj = u.configStylePropertyObject(property);
  117. for (var i in obj) {
  118. if (obj.hasOwnProperty(i)) {
  119. if (typeof values === 'string') {
  120. obj[i] = values + ' ' + time;
  121. } else {
  122. var st = '';
  123. for (var j = 0, max = values.length; j < max; j += 1) {
  124. st += values[j] + ' ' + time + ', ';
  125. }
  126. obj[i] = st.slice(0, -2);
  127. }
  128. }
  129. }
  130. return obj;
  131. };
  132. u.getVendorStyle = function (property, value) {
  133. var obj = u.configStylePropertyObject(property);
  134. for (var i in obj) {
  135. if (obj.hasOwnProperty(i)) {
  136. obj[i] = value;
  137. }
  138. }
  139. return obj;
  140. };
  141. u.configStylePropertyObject = function (prop) {
  142. var obj = {};
  143. obj[prop] = '';
  144. var vendors = ['webkit', 'Moz', 'o'];
  145. vendors.forEach(function (vendor) {
  146. obj[vendor + prop.charAt(0).toUpperCase() + prop.slice(1)] = '';
  147. });
  148. return obj;
  149. };
  150. u.extend = function (objA, objB) {
  151. for (var i in objB) {
  152. if (objB.hasOwnProperty(i)) {
  153. objA[i] = objB[i];
  154. }
  155. }
  156. return objA;
  157. };
  158. // Overwrite only what's already present
  159. u.safeExtend = function (objA, objB) {
  160. var obj = {};
  161. for (var i in objA) {
  162. if (objA.hasOwnProperty(i) && objB.hasOwnProperty(i)) {
  163. obj[i] = objB[i];
  164. } else if (objA.hasOwnProperty(i)) {
  165. obj[i] = objA[i];
  166. }
  167. }
  168. return obj;
  169. };
  170. // Map for array or unique item.
  171. u.map = function (ar, fn) {
  172. if (ar.length) {
  173. for (var i = 0, max = ar.length; i < max; i += 1) {
  174. fn(ar[i]);
  175. }
  176. } else {
  177. fn(ar);
  178. }
  179. };
  180. ///////////////////////
  181. /// SUPER CLASS ///
  182. ///////////////////////
  183. function Super () {};
  184. // Basic event system.
  185. Super.prototype.on = function (arg, cb) {
  186. var self = this;
  187. var types = arg.split(/[ ,]+/g);
  188. var type;
  189. self._handlers_ = self._handlers_ || {};
  190. for (var i = 0; i < types.length; i += 1) {
  191. type = types[i];
  192. self._handlers_[type] = self._handlers_[type] || [];
  193. self._handlers_[type].push(cb);
  194. }
  195. return self;
  196. };
  197. Super.prototype.off = function (type, cb) {
  198. var self = this;
  199. self._handlers_ = self._handlers_ || {};
  200. if (type === undefined) {
  201. self._handlers_ = {};
  202. } else if (cb === undefined) {
  203. self._handlers_[type] = null;
  204. } else if (self._handlers_[type] &&
  205. self._handlers_[type].indexOf(cb) >= 0) {
  206. self._handlers_[type].splice(self._handlers_[type].indexOf(cb), 1);
  207. }
  208. return self;
  209. };
  210. Super.prototype.trigger = function (arg, data) {
  211. var self = this;
  212. var types = arg.split(/[ ,]+/g);
  213. var type;
  214. self._handlers_ = self._handlers_ || {};
  215. for (var i = 0; i < types.length; i += 1) {
  216. type = types[i];
  217. if (self._handlers_[type] && self._handlers_[type].length) {
  218. self._handlers_[type].forEach(function (handler) {
  219. handler.call(self, {
  220. type: type,
  221. target: self
  222. }, data);
  223. });
  224. }
  225. }
  226. };
  227. // Configuration
  228. Super.prototype.config = function (options) {
  229. var self = this;
  230. self.options = self.defaults || {};
  231. if (options) {
  232. self.options = u.safeExtend(self.options, options);
  233. }
  234. };
  235. // Bind internal events.
  236. Super.prototype.bindEvt = function (el, type) {
  237. var self = this;
  238. self._domHandlers_ = self._domHandlers_ || {};
  239. self._domHandlers_[type] = function () {
  240. if (typeof self['on' + type] === 'function') {
  241. self['on' + type].apply(self, arguments);
  242. } else {
  243. console.warn('[WARNING] : Missing "on' + type + '" handler.');
  244. }
  245. };
  246. u.bindEvt(el, toBind[type], self._domHandlers_[type]);
  247. if (secondBind[type]) {
  248. // Support for both touch and mouse at the same time.
  249. u.bindEvt(el, secondBind[type], self._domHandlers_[type]);
  250. }
  251. return self;
  252. };
  253. // Unbind dom events.
  254. Super.prototype.unbindEvt = function (el, type) {
  255. var self = this;
  256. self._domHandlers_ = self._domHandlers_ || {};
  257. u.unbindEvt(el, toBind[type], self._domHandlers_[type]);
  258. if (secondBind[type]) {
  259. // Support for both touch and mouse at the same time.
  260. u.unbindEvt(el, secondBind[type], self._domHandlers_[type]);
  261. }
  262. delete self._domHandlers_[type];
  263. return this;
  264. };
  265. ///////////////////////
  266. /// THE NIPPLE ///
  267. ///////////////////////
  268. function Nipple (collection, options) {
  269. this.identifier = options.identifier;
  270. this.position = options.position;
  271. this.frontPosition = options.frontPosition;
  272. this.collection = collection;
  273. // Defaults
  274. this.defaults = {
  275. size: 100,
  276. threshold: 0.1,
  277. color: 'white',
  278. fadeTime: 250,
  279. dataOnly: false,
  280. restOpacity: 0.5,
  281. mode: 'dynamic',
  282. zone: document.body
  283. };
  284. this.config(options);
  285. // Overwrites
  286. if (this.options.mode === 'dynamic') {
  287. this.options.restOpacity = 0;
  288. }
  289. this.id = Nipple.id;
  290. Nipple.id += 1;
  291. this.buildEl()
  292. .stylize();
  293. // Nipple's API.
  294. this.instance = {
  295. el: this.ui.el,
  296. on: this.on.bind(this),
  297. off: this.off.bind(this),
  298. show: this.show.bind(this),
  299. hide: this.hide.bind(this),
  300. add: this.addToDom.bind(this),
  301. remove: this.removeFromDom.bind(this),
  302. destroy: this.destroy.bind(this),
  303. resetDirection: this.resetDirection.bind(this),
  304. computeDirection: this.computeDirection.bind(this),
  305. trigger: this.trigger.bind(this),
  306. position: this.position,
  307. frontPosition: this.frontPosition,
  308. ui: this.ui,
  309. identifier: this.identifier,
  310. id: this.id,
  311. options: this.options
  312. };
  313. return this.instance;
  314. };
  315. Nipple.prototype = new Super();
  316. Nipple.constructor = Nipple;
  317. Nipple.id = 0;
  318. // Build the dom element of the Nipple instance.
  319. Nipple.prototype.buildEl = function (options) {
  320. this.ui = {};
  321. if (this.options.dataOnly) {
  322. return this;
  323. }
  324. this.ui.el = document.createElement('div');
  325. this.ui.back = document.createElement('div');
  326. this.ui.front = document.createElement('div');
  327. this.ui.el.className = 'nipple collection_' + this.collection.id;
  328. this.ui.back.className = 'back';
  329. this.ui.front.className = 'front';
  330. this.ui.el.setAttribute('id', 'nipple_' + this.collection.id +
  331. '_' + this.id);
  332. this.ui.el.appendChild(this.ui.back);
  333. this.ui.el.appendChild(this.ui.front);
  334. return this;
  335. };
  336. // Apply CSS to the Nipple instance.
  337. Nipple.prototype.stylize = function () {
  338. if (this.options.dataOnly) {
  339. return this;
  340. }
  341. var animTime = this.options.fadeTime + 'ms';
  342. var borderStyle = u.getVendorStyle('borderRadius', '50%');
  343. var transitStyle = u.getTransitionStyle('transition', 'opacity', animTime);
  344. var styles = {};
  345. styles.el = {
  346. position: 'absolute',
  347. opacity: this.options.restOpacity,
  348. display: 'block',
  349. 'zIndex': 999
  350. };
  351. styles.back = {
  352. position: 'absolute',
  353. display: 'block',
  354. width: this.options.size + 'px',
  355. height: this.options.size + 'px',
  356. marginLeft: -this.options.size / 2 + 'px',
  357. marginTop: -this.options.size / 2 + 'px',
  358. background: this.options.color,
  359. 'opacity': '.5'
  360. };
  361. styles.front = {
  362. width: this.options.size / 2 + 'px',
  363. height: this.options.size / 2 + 'px',
  364. position: 'absolute',
  365. display: 'block',
  366. marginLeft: -this.options.size / 4 + 'px',
  367. marginTop: -this.options.size / 4 + 'px',
  368. background: this.options.color,
  369. 'opacity': '.5'
  370. };
  371. u.extend(styles.el, transitStyle);
  372. u.extend(styles.back, borderStyle);
  373. u.extend(styles.front, borderStyle);
  374. this.applyStyles(styles);
  375. return this;
  376. };
  377. Nipple.prototype.applyStyles = function (styles) {
  378. // Apply styles
  379. for (var i in this.ui) {
  380. if (this.ui.hasOwnProperty(i)) {
  381. for (var j in styles[i]) {
  382. this.ui[i].style[j] = styles[i][j];
  383. }
  384. }
  385. }
  386. return this;
  387. };
  388. // Inject the Nipple instance into DOM.
  389. Nipple.prototype.addToDom = function () {
  390. // We're not adding it if we're dataOnly or already in dom.
  391. if (this.options.dataOnly || document.body.contains(this.ui.el)) {
  392. return this;
  393. }
  394. this.options.zone.appendChild(this.ui.el);
  395. return this;
  396. };
  397. // Remove the Nipple instance from DOM.
  398. Nipple.prototype.removeFromDom = function () {
  399. if (this.options.dataOnly || !document.body.contains(this.ui.el)) {
  400. return this;
  401. }
  402. this.options.zone.removeChild(this.ui.el);
  403. return this;
  404. };
  405. // Entirely destroy this nipple
  406. Nipple.prototype.destroy = function () {
  407. clearTimeout(this.removeTimeout);
  408. clearTimeout(this.showTimeout);
  409. clearTimeout(this.restTimeout);
  410. this.trigger('destroyed', this.instance);
  411. this.removeFromDom();
  412. this.off();
  413. };
  414. // Fade in the Nipple instance.
  415. Nipple.prototype.show = function (cb) {
  416. var self = this;
  417. if (self.options.dataOnly) {
  418. return self;
  419. }
  420. clearTimeout(self.removeTimeout);
  421. clearTimeout(self.showTimeout);
  422. clearTimeout(self.restTimeout);
  423. self.addToDom();
  424. self.restCallback();
  425. setTimeout(function () {
  426. self.ui.el.style.opacity = 1;
  427. }, 0);
  428. self.showTimeout = setTimeout(function () {
  429. self.trigger('shown', self.instance);
  430. if (typeof cb === 'function') {
  431. cb.call(this);
  432. }
  433. }, self.options.fadeTime);
  434. return self;
  435. };
  436. // Fade out the Nipple instance.
  437. Nipple.prototype.hide = function (cb) {
  438. var self = this;
  439. if (self.options.dataOnly) {
  440. return self;
  441. }
  442. self.ui.el.style.opacity = self.options.restOpacity;
  443. clearTimeout(self.removeTimeout);
  444. clearTimeout(self.showTimeout);
  445. clearTimeout(self.restTimeout);
  446. self.removeTimeout = setTimeout(
  447. function () {
  448. var display = self.options.mode === 'dynamic' ? 'none' : 'block';
  449. self.ui.el.style.display = display;
  450. if (typeof cb === 'function') {
  451. cb.call(self);
  452. }
  453. self.trigger('hidden', self.instance);
  454. },
  455. self.options.fadeTime
  456. );
  457. self.restPosition();
  458. return self;
  459. };
  460. Nipple.prototype.restPosition = function (cb) {
  461. var self = this;
  462. self.frontPosition = {
  463. x: 0,
  464. y: 0
  465. };
  466. var animTime = self.options.fadeTime + 'ms';
  467. var transitStyle = {};
  468. transitStyle.front = u.getTransitionStyle('transition',
  469. ['top', 'left'], animTime);
  470. var styles = {front: {}};
  471. styles.front = {
  472. left: self.frontPosition.x + 'px',
  473. top: self.frontPosition.y + 'px'
  474. };
  475. self.applyStyles(transitStyle);
  476. self.applyStyles(styles);
  477. self.restTimeout = setTimeout(
  478. function () {
  479. if (typeof cb === 'function') {
  480. cb.call(self);
  481. }
  482. self.restCallback();
  483. },
  484. self.options.fadeTime
  485. );
  486. };
  487. Nipple.prototype.restCallback = function () {
  488. var self = this;
  489. var transitStyle = {};
  490. transitStyle.front = u.getTransitionStyle('transition', 'none', '');
  491. self.applyStyles(transitStyle);
  492. self.trigger('rested', self.instance);
  493. };
  494. Nipple.prototype.resetDirection = function () {
  495. // Fully rebuild the object to let the iteration possible.
  496. this.direction = {
  497. x: false,
  498. y: false,
  499. angle: false
  500. };
  501. };
  502. Nipple.prototype.computeDirection = function (obj) {
  503. var rAngle = obj.angle.radian;
  504. var angle45 = Math.PI / 4;
  505. var angle90 = Math.PI / 2;
  506. var direction, directionX, directionY;
  507. // Angular direction
  508. // \ UP /
  509. // \ /
  510. // LEFT RIGHT
  511. // / \
  512. // /DOWN \
  513. //
  514. if (rAngle > angle45 && rAngle < (angle45 * 3)) {
  515. direction = 'up';
  516. } else if (rAngle > -angle45 && rAngle <= angle45) {
  517. direction = 'left';
  518. } else if (rAngle > (-angle45 * 3) && rAngle <= -angle45) {
  519. direction = 'down';
  520. } else {
  521. direction = 'right';
  522. }
  523. // Plain direction
  524. // UP |
  525. // _______ | RIGHT
  526. // LEFT |
  527. // DOWN |
  528. if (rAngle > -angle90 && rAngle < angle90) {
  529. directionX = 'left';
  530. } else {
  531. directionX = 'right';
  532. }
  533. if (rAngle > 0) {
  534. directionY = 'up';
  535. } else {
  536. directionY = 'down';
  537. }
  538. if (obj.force > this.options.threshold) {
  539. var oldDirection = {};
  540. for (var i in this.direction) {
  541. if (this.direction.hasOwnProperty(i)) {
  542. oldDirection[i] = this.direction[i];
  543. }
  544. }
  545. var same = {};
  546. this.direction = {
  547. x: directionX,
  548. y: directionY,
  549. angle: direction
  550. };
  551. obj.direction = this.direction;
  552. for (var i in oldDirection) {
  553. if (oldDirection[i] === this.direction[i]) {
  554. same[i] = true;
  555. }
  556. }
  557. // If all 3 directions are the same, we don't trigger anything.
  558. if (same.x && same.y && same.angle) {
  559. return obj;
  560. }
  561. if (!same.x || !same.y) {
  562. this.trigger('plain', obj);
  563. }
  564. if (!same.x) {
  565. this.trigger('plain:' + directionX, obj);
  566. }
  567. if (!same.y) {
  568. this.trigger('plain:' + directionY, obj);
  569. }
  570. if (!same.angle) {
  571. this.trigger('dir dir:' + direction, obj);
  572. }
  573. }
  574. return obj;
  575. };
  576. /* global Nipple, Super */
  577. ///////////////////////////
  578. /// THE COLLECTION ///
  579. ///////////////////////////
  580. function Collection (manager, options) {
  581. var self = this;
  582. self.nipples = [];
  583. self.idles = [];
  584. self.actives = [];
  585. self.ids = [];
  586. self.pressureIntervals = {};
  587. self.manager = manager;
  588. self.id = Collection.id;
  589. Collection.id += 1;
  590. // Defaults
  591. self.defaults = {
  592. zone: document.body,
  593. multitouch: false,
  594. maxNumberOfNipples: 10,
  595. mode: 'dynamic',
  596. position: {top: 0, left: 0},
  597. catchDistance: 200,
  598. size: 100,
  599. threshold: 0.1,
  600. color: 'white',
  601. fadeTime: 250,
  602. dataOnly: false,
  603. restOpacity: 0.5
  604. };
  605. self.config(options);
  606. // Overwrites
  607. if (self.options.mode === 'static' || self.options.mode === 'semi') {
  608. self.options.multitouch = false;
  609. }
  610. if (!self.options.multitouch) {
  611. self.options.maxNumberOfNipples = 1;
  612. }
  613. self.updateBox();
  614. self.prepareNipples();
  615. self.bindings();
  616. self.begin();
  617. return self.nipples;
  618. }
  619. Collection.prototype = new Super();
  620. Collection.constructor = Collection;
  621. Collection.id = 0;
  622. Collection.prototype.prepareNipples = function () {
  623. var self = this;
  624. var nips = self.nipples;
  625. // Public API Preparation.
  626. nips.on = self.on.bind(self);
  627. nips.off = self.off.bind(self);
  628. nips.options = self.options;
  629. nips.destroy = self.destroy.bind(self);
  630. nips.ids = self.ids;
  631. nips.id = self.id;
  632. nips.processOnMove = self.processOnMove.bind(self);
  633. nips.processOnEnd = self.processOnEnd.bind(self);
  634. nips.get = function (id) {
  635. if (id === undefined) {
  636. return nips[0];
  637. }
  638. for (var i = 0, max = nips.length; i < max; i += 1) {
  639. if (nips[i].identifier === id) {
  640. return nips[i];
  641. }
  642. }
  643. return false;
  644. };
  645. };
  646. Collection.prototype.bindings = function () {
  647. var self = this;
  648. // Touch start event.
  649. self.bindEvt(self.options.zone, 'start');
  650. // Avoid native touch actions (scroll, zoom etc...) on the zone.
  651. self.options.zone.style.touchAction = 'none';
  652. self.options.zone.style.msTouchAction = 'none';
  653. };
  654. Collection.prototype.begin = function () {
  655. var self = this;
  656. var opts = self.options;
  657. // We place our static nipple
  658. // if needed.
  659. if (opts.mode === 'static') {
  660. var nipple = self.createNipple(
  661. opts.position,
  662. self.manager.getIdentifier()
  663. );
  664. // Add it to the dom.
  665. nipple.add();
  666. // Store it in idles.
  667. self.idles.push(nipple);
  668. }
  669. };
  670. // Nipple Factory
  671. Collection.prototype.createNipple = function (position, identifier) {
  672. var self = this;
  673. var scroll = u.getScroll();
  674. var toPutOn = {};
  675. var opts = self.options;
  676. if (position.x && position.y) {
  677. toPutOn = {
  678. x: position.x -
  679. (scroll.x + self.box.left),
  680. y: position.y -
  681. (scroll.y + self.box.top)
  682. };
  683. } else if (
  684. position.top ||
  685. position.right ||
  686. position.bottom ||
  687. position.left
  688. ) {
  689. // We need to compute the position X / Y of the joystick.
  690. var dumb = document.createElement('DIV');
  691. dumb.style.display = 'hidden';
  692. dumb.style.top = position.top;
  693. dumb.style.right = position.right;
  694. dumb.style.bottom = position.bottom;
  695. dumb.style.left = position.left;
  696. dumb.style.position = 'absolute';
  697. opts.zone.appendChild(dumb);
  698. var dumbBox = dumb.getBoundingClientRect();
  699. opts.zone.removeChild(dumb);
  700. toPutOn = position;
  701. position = {
  702. x: dumbBox.left + scroll.x,
  703. y: dumbBox.top + scroll.y
  704. };
  705. }
  706. var nipple = new Nipple(self, {
  707. color: opts.color,
  708. size: opts.size,
  709. threshold: opts.threshold,
  710. fadeTime: opts.fadeTime,
  711. dataOnly: opts.dataOnly,
  712. restOpacity: opts.restOpacity,
  713. mode: opts.mode,
  714. identifier: identifier,
  715. position: position,
  716. zone: opts.zone,
  717. frontPosition: {
  718. x: 0,
  719. y: 0
  720. }
  721. });
  722. if (!opts.dataOnly) {
  723. u.applyPosition(nipple.ui.el, toPutOn);
  724. u.applyPosition(nipple.ui.front, nipple.frontPosition);
  725. }
  726. self.nipples.push(nipple);
  727. self.trigger('added ' + nipple.identifier + ':added', nipple);
  728. self.manager.trigger('added ' + nipple.identifier + ':added', nipple);
  729. self.bindNipple(nipple);
  730. return nipple;
  731. };
  732. Collection.prototype.updateBox = function () {
  733. var self = this;
  734. self.box = self.options.zone.getBoundingClientRect();
  735. };
  736. Collection.prototype.bindNipple = function (nipple) {
  737. var self = this;
  738. var type;
  739. // Bubble up identified events.
  740. var handler = function (evt, data) {
  741. // Identify the event type with the nipple's id.
  742. type = evt.type + ' ' + data.id + ':' + evt.type;
  743. self.trigger(type, data);
  744. };
  745. // When it gets destroyed.
  746. nipple.on('destroyed', self.onDestroyed.bind(self));
  747. // Other events that will get bubbled up.
  748. nipple.on('shown hidden rested dir plain', handler);
  749. nipple.on('dir:up dir:right dir:down dir:left', handler);
  750. nipple.on('plain:up plain:right plain:down plain:left', handler);
  751. };
  752. Collection.prototype.pressureFn = function (touch, nipple, identifier) {
  753. var self = this;
  754. var previousPressure = 0;
  755. clearInterval(self.pressureIntervals[identifier]);
  756. // Create an interval that will read the pressure every 100ms
  757. self.pressureIntervals[identifier] = setInterval(function () {
  758. var pressure = touch.force || touch.pressure ||
  759. touch.webkitForce || 0;
  760. if (pressure !== previousPressure) {
  761. nipple.trigger('pressure', pressure);
  762. self.trigger('pressure ' +
  763. nipple.identifier + ':pressure', pressure);
  764. previousPressure = pressure;
  765. }
  766. }.bind(self), 100);
  767. };
  768. Collection.prototype.onstart = function (evt) {
  769. var self = this;
  770. var opts = self.options;
  771. evt = u.prepareEvent(evt);
  772. // Update the box position
  773. self.updateBox();
  774. var process = function (touch) {
  775. // If we can create new nipples
  776. // meaning we don't have more active nipples than we should.
  777. if (self.actives.length < opts.maxNumberOfNipples) {
  778. self.processOnStart(touch);
  779. }
  780. };
  781. u.map(evt, process);
  782. // We ask upstream to bind the document
  783. // on 'move' and 'end'
  784. self.manager.bindDocument();
  785. return false;
  786. };
  787. Collection.prototype.processOnStart = function (evt) {
  788. var self = this;
  789. var opts = self.options;
  790. var indexInIdles;
  791. var identifier = self.manager.getIdentifier(evt);
  792. var pressure = evt.force || evt.pressure || evt.webkitForce || 0;
  793. var position = {
  794. x: evt.pageX,
  795. y: evt.pageY
  796. };
  797. var nipple = self.getOrCreate(identifier, position);
  798. // Update its touch identifier
  799. nipple.identifier = identifier;
  800. var process = function (nip) {
  801. // Trigger the start.
  802. nip.trigger('start', nip);
  803. self.trigger('start ' + nip.id + ':start', nip);
  804. nip.show();
  805. if (pressure > 0) {
  806. self.pressureFn(evt, nip, nip.identifier);
  807. }
  808. // Trigger the first move event.
  809. self.processOnMove(evt);
  810. };
  811. // Transfer it from idles to actives.
  812. if ((indexInIdles = self.idles.indexOf(nipple)) >= 0) {
  813. self.idles.splice(indexInIdles, 1);
  814. }
  815. // Store the nipple in the actives array
  816. self.actives.push(nipple);
  817. self.ids.push(nipple.identifier);
  818. if (opts.mode !== 'semi') {
  819. process(nipple);
  820. } else {
  821. // In semi we check the distance of the touch
  822. // to decide if we have to reset the nipple
  823. var distance = u.distance(position, nipple.position);
  824. if (distance <= opts.catchDistance) {
  825. process(nipple);
  826. } else {
  827. nipple.destroy();
  828. self.processOnStart(evt);
  829. return;
  830. }
  831. }
  832. return nipple;
  833. };
  834. Collection.prototype.getOrCreate = function (identifier, position) {
  835. var self = this;
  836. var opts = self.options;
  837. var nipple;
  838. // If we're in static or semi, we might already have an active.
  839. if (/(semi|static)/.test(opts.mode)) {
  840. // Get the active one.
  841. // TODO: Multi-touche for semi and static will start here.
  842. // Return the nearest one.
  843. nipple = self.idles[0];
  844. if (nipple) {
  845. self.idles.splice(0, 1);
  846. return nipple;
  847. }
  848. if (opts.mode === 'semi') {
  849. // If we're in semi mode, we need to create one.
  850. return self.createNipple(position, identifier);
  851. }
  852. console.warn('Coudln\'t find the needed nipple.');
  853. return false;
  854. }
  855. // In dynamic, we create a new one.
  856. nipple = self.createNipple(position, identifier);
  857. return nipple;
  858. };
  859. Collection.prototype.processOnMove = function (evt) {
  860. var self = this;
  861. var opts = self.options;
  862. var identifier = self.manager.getIdentifier(evt);
  863. var nipple = self.nipples.get(identifier);
  864. if (!nipple) {
  865. // This is here just for safety.
  866. // It shouldn't happen.
  867. console.error('Found zombie joystick with ID ' + identifier);
  868. self.manager.removeIdentifier(identifier);
  869. return;
  870. }
  871. nipple.identifier = identifier;
  872. var size = nipple.options.size / 2;
  873. var pos = {
  874. x: evt.pageX,
  875. y: evt.pageY
  876. };
  877. var dist = u.distance(pos, nipple.position);
  878. var angle = u.angle(pos, nipple.position);
  879. var rAngle = u.radians(angle);
  880. var force = dist / size;
  881. // If distance is bigger than nipple's size
  882. // we clamp the position.
  883. if (dist > size) {
  884. dist = size;
  885. pos = u.findCoord(nipple.position, dist, angle);
  886. }
  887. nipple.frontPosition = {
  888. x: pos.x - nipple.position.x,
  889. y: pos.y - nipple.position.y
  890. };
  891. if (!opts.dataOnly) {
  892. u.applyPosition(nipple.ui.front, nipple.frontPosition);
  893. }
  894. // Prepare event's datas.
  895. var toSend = {
  896. identifier: nipple.identifier,
  897. position: pos,
  898. force: force,
  899. pressure: evt.force || evt.pressure || evt.webkitForce || 0,
  900. distance: dist,
  901. angle: {
  902. radian: rAngle,
  903. degree: angle
  904. },
  905. instance: nipple
  906. };
  907. // Compute the direction's datas.
  908. toSend = nipple.computeDirection(toSend);
  909. // Offset angles to follow units circle.
  910. toSend.angle = {
  911. radian: u.radians(180 - angle),
  912. degree: 180 - angle
  913. };
  914. // Send everything to everyone.
  915. nipple.trigger('move', toSend);
  916. self.trigger('move ' + nipple.id + ':move', toSend);
  917. };
  918. Collection.prototype.processOnEnd = function (evt) {
  919. var self = this;
  920. var opts = self.options;
  921. var identifier = self.manager.getIdentifier(evt);
  922. var nipple = self.nipples.get(identifier);
  923. var removedIdentifier = self.manager.removeIdentifier(nipple.identifier);
  924. if (!nipple) {
  925. return;
  926. }
  927. if (!opts.dataOnly) {
  928. nipple.hide(function () {
  929. if (opts.mode === 'dynamic') {
  930. nipple.trigger('removed', nipple);
  931. self.trigger('removed ' + nipple.id + ':removed', nipple);
  932. self.manager
  933. .trigger('removed ' + nipple.id + ':removed', nipple);
  934. nipple.destroy();
  935. }
  936. });
  937. }
  938. // Clear the pressure interval reader
  939. clearInterval(self.pressureIntervals[nipple.identifier]);
  940. // Reset the direciton of the nipple, to be able to trigger a new direction
  941. // on start.
  942. nipple.resetDirection();
  943. nipple.trigger('end', nipple);
  944. self.trigger('end ' + nipple.id + ':end', nipple);
  945. // Remove identifier from our bank.
  946. if (self.ids.indexOf(nipple.identifier) >= 0) {
  947. self.ids.splice(self.ids.indexOf(nipple.identifier), 1);
  948. }
  949. // Clean our actives array.
  950. if (self.actives.indexOf(nipple) >= 0) {
  951. self.actives.splice(self.actives.indexOf(nipple), 1);
  952. }
  953. if (/(semi|static)/.test(opts.mode)) {
  954. // Transfer nipple from actives to idles
  955. // if we're in semi or static mode.
  956. self.idles.push(nipple);
  957. } else if (self.nipples.indexOf(nipple) >= 0) {
  958. // Only if we're not in semi or static mode
  959. // we can remove the instance.
  960. self.nipples.splice(self.nipples.indexOf(nipple), 1);
  961. }
  962. // We unbind move and end.
  963. self.manager.unbindDocument();
  964. // We add back the identifier of the idle nipple;
  965. if (/(semi|static)/.test(opts.mode)) {
  966. self.manager.ids[removedIdentifier.id] = removedIdentifier.identifier;
  967. }
  968. };
  969. // Remove destroyed nipple from the lists
  970. Collection.prototype.onDestroyed = function(evt, nipple) {
  971. var self = this;
  972. if (self.nipples.indexOf(nipple) >= 0) {
  973. self.nipples.splice(self.nipples.indexOf(nipple), 1);
  974. }
  975. if (self.actives.indexOf(nipple) >= 0) {
  976. self.actives.splice(self.actives.indexOf(nipple), 1);
  977. }
  978. if (self.idles.indexOf(nipple) >= 0) {
  979. self.idles.splice(self.idles.indexOf(nipple), 1);
  980. }
  981. if (self.ids.indexOf(nipple.identifier) >= 0) {
  982. self.ids.splice(self.ids.indexOf(nipple.identifier), 1);
  983. }
  984. // Remove the identifier from our bank
  985. self.manager.removeIdentifier(nipple.identifier);
  986. // We unbind move and end.
  987. self.manager.unbindDocument();
  988. };
  989. // Cleanly destroy the manager
  990. Collection.prototype.destroy = function () {
  991. var self = this;
  992. self.unbindEvt(self.options.zone, 'start');
  993. // Destroy nipples.
  994. self.nipples.forEach(function(nipple) {
  995. nipple.destroy();
  996. });
  997. // Clean 3DTouch intervals.
  998. for (var i in self.pressureIntervals) {
  999. if (self.pressureIntervals.hasOwnProperty(i)) {
  1000. clearInterval(self.pressureIntervals[i]);
  1001. }
  1002. }
  1003. // Notify the manager passing the instance
  1004. self.trigger('destroyed', self.nipples);
  1005. // We unbind move and end.
  1006. self.manager.unbindDocument();
  1007. // Unbind everything.
  1008. self.off();
  1009. };
  1010. /* global u, Super, Collection */
  1011. ///////////////////////
  1012. /// MANAGER ///
  1013. ///////////////////////
  1014. function Manager (options) {
  1015. var self = this;
  1016. self.ids = {};
  1017. self.index = 0;
  1018. self.collections = [];
  1019. self.config(options);
  1020. self.prepareCollections();
  1021. // Listen for resize, to reposition every joysticks
  1022. var resizeTimer;
  1023. u.bindEvt(window, 'resize', function (evt) {
  1024. clearTimeout(resizeTimer);
  1025. resizeTimer = setTimeout(function () {
  1026. var pos;
  1027. var scroll = u.getScroll();
  1028. self.collections.forEach(function (collection) {
  1029. collection.forEach(function (nipple) {
  1030. pos = nipple.el.getBoundingClientRect();
  1031. nipple.position = {
  1032. x: scroll.x + pos.left,
  1033. y: scroll.y + pos.top
  1034. };
  1035. });
  1036. });
  1037. }, 100);
  1038. });
  1039. return self.collections;
  1040. };
  1041. Manager.prototype = new Super();
  1042. Manager.constructor = Manager;
  1043. Manager.prototype.prepareCollections = function () {
  1044. var self = this;
  1045. // Public API Preparation.
  1046. self.collections.create = self.create.bind(self);
  1047. // Listen to anything
  1048. self.collections.on = self.on.bind(self);
  1049. // Unbind general events
  1050. self.collections.off = self.off.bind(self);
  1051. // Destroy everything
  1052. self.collections.destroy = self.destroy.bind(self);
  1053. // Get any nipple
  1054. self.collections.get = function (id) {
  1055. var nipple;
  1056. self.collections.every(function (collection) {
  1057. if (nipple = collection.get(id)) {
  1058. return false;
  1059. }
  1060. return true;
  1061. });
  1062. return nipple;
  1063. };
  1064. };
  1065. Manager.prototype.create = function (options) {
  1066. return this.createCollection(options);
  1067. };
  1068. // Collection Factory
  1069. Manager.prototype.createCollection = function (options) {
  1070. var self = this;
  1071. var collection = new Collection(self, options);
  1072. self.bindCollection(collection);
  1073. self.collections.push(collection);
  1074. return collection;
  1075. };
  1076. Manager.prototype.bindCollection = function (collection) {
  1077. var self = this;
  1078. var type;
  1079. // Bubble up identified events.
  1080. var handler = function (evt, data) {
  1081. // Identify the event type with the nipple's identifier.
  1082. type = evt.type + ' ' + data.id + ':' + evt.type;
  1083. self.trigger(type, data);
  1084. };
  1085. // When it gets destroyed we clean.
  1086. collection.on('destroyed', self.onDestroyed.bind(self));
  1087. // Other events that will get bubbled up.
  1088. collection.on('shown hidden rested dir plain', handler);
  1089. collection.on('dir:up dir:right dir:down dir:left', handler);
  1090. collection.on('plain:up plain:right plain:down plain:left', handler);
  1091. };
  1092. Manager.prototype.bindDocument = function () {
  1093. var self = this;
  1094. // Bind only if not already binded
  1095. if (!self.binded) {
  1096. self.bindEvt(document, 'move')
  1097. .bindEvt(document, 'end');
  1098. self.binded = true;
  1099. }
  1100. };
  1101. Manager.prototype.unbindDocument = function (force) {
  1102. var self = this;
  1103. // If there are no touch left
  1104. // unbind the document.
  1105. if (!Object.keys(self.ids).length || force === true) {
  1106. self.unbindEvt(document, 'move')
  1107. .unbindEvt(document, 'end');
  1108. self.binded = false;
  1109. }
  1110. };
  1111. Manager.prototype.getIdentifier = function (evt) {
  1112. var id;
  1113. // If no event, simple increment
  1114. if (!evt) {
  1115. id = this.index;
  1116. } else {
  1117. // Extract identifier from event object.
  1118. // Unavailable in mouse events so replaced by latest increment.
  1119. id = evt.identifier === undefined ? evt.pointerId : evt.identifier;
  1120. if (id === undefined) {
  1121. id = this.latest || 0;
  1122. }
  1123. }
  1124. if (this.ids[id] === undefined) {
  1125. this.ids[id] = this.index;
  1126. this.index += 1;
  1127. }
  1128. // Keep the latest id used in case we're using an unidentified mouseEvent
  1129. this.latest = id;
  1130. return this.ids[id];
  1131. };
  1132. Manager.prototype.removeIdentifier = function (identifier) {
  1133. var removed = {};
  1134. for (var id in this.ids) {
  1135. if (this.ids[id] === identifier) {
  1136. removed.id = id;
  1137. removed.identifier = this.ids[id];
  1138. delete this.ids[id];
  1139. break;
  1140. }
  1141. }
  1142. return removed;
  1143. };
  1144. Manager.prototype.onmove = function (evt) {
  1145. var self = this;
  1146. self.onAny('move', evt);
  1147. return false;
  1148. };
  1149. Manager.prototype.onend = function (evt) {
  1150. var self = this;
  1151. self.onAny('end', evt);
  1152. return false;
  1153. };
  1154. Manager.prototype.onAny = function (which, evt) {
  1155. var self = this;
  1156. var id;
  1157. var processFn = 'processOn' + which.charAt(0).toUpperCase() +
  1158. which.slice(1);
  1159. evt = u.prepareEvent(evt);
  1160. var processColl = function (e, id, coll) {
  1161. if (coll.ids.indexOf(id) >= 0) {
  1162. coll[processFn](e);
  1163. // Mark the event to avoid cleaning it later.
  1164. e._found_ = true;
  1165. }
  1166. };
  1167. var processEvt = function (e) {
  1168. id = self.getIdentifier(e);
  1169. u.map(self.collections, processColl.bind(null, e, id));
  1170. // If the event isn't handled by any collection,
  1171. // we need to clean its identifier.
  1172. if (!e._found_) {
  1173. self.removeIdentifier(id);
  1174. }
  1175. };
  1176. u.map(evt, processEvt);
  1177. return false;
  1178. };
  1179. // Cleanly destroy the manager
  1180. Manager.prototype.destroy = function () {
  1181. var self = this;
  1182. self.unbindDocument(true);
  1183. self.ids = {};
  1184. self.index = 0;
  1185. self.collections.forEach(function(collection) {
  1186. collection.destroy();
  1187. });
  1188. self.off();
  1189. };
  1190. // When a collection gets destroyed
  1191. // we clean behind.
  1192. Manager.prototype.onDestroyed = function (evt, coll) {
  1193. var self = this;
  1194. if (self.collections.indexOf(coll) < 0) {
  1195. return false;
  1196. }
  1197. self.collections.splice(self.collections.indexOf(coll), 1);
  1198. };
  1199. var factory = new Manager();
  1200. return {
  1201. create: function (options) {
  1202. return factory.create(options);
  1203. },
  1204. factory: factory
  1205. };
  1206. });