nipple.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. import Super from './super';
  2. import * as u from './utils';
  3. ///////////////////////
  4. /// THE NIPPLE ///
  5. ///////////////////////
  6. function Nipple (collection, options) {
  7. this.identifier = options.identifier;
  8. this.position = options.position;
  9. this.frontPosition = options.frontPosition;
  10. this.collection = collection;
  11. // Defaults
  12. this.defaults = {
  13. size: 100,
  14. threshold: 0.1,
  15. color: 'white',
  16. fadeTime: 250,
  17. dataOnly: false,
  18. restJoystick: true,
  19. restOpacity: 0.5,
  20. mode: 'dynamic',
  21. zone: document.body,
  22. lockX: false,
  23. lockY: false,
  24. shape: 'circle'
  25. };
  26. this.config(options);
  27. // Overwrites
  28. if (this.options.mode === 'dynamic') {
  29. this.options.restOpacity = 0;
  30. }
  31. this.id = Nipple.id;
  32. Nipple.id += 1;
  33. this.buildEl()
  34. .stylize();
  35. // Nipple's API.
  36. this.instance = {
  37. el: this.ui.el,
  38. on: this.on.bind(this),
  39. off: this.off.bind(this),
  40. show: this.show.bind(this),
  41. hide: this.hide.bind(this),
  42. add: this.addToDom.bind(this),
  43. remove: this.removeFromDom.bind(this),
  44. destroy: this.destroy.bind(this),
  45. setPosition:this.setPosition.bind(this),
  46. resetDirection: this.resetDirection.bind(this),
  47. computeDirection: this.computeDirection.bind(this),
  48. trigger: this.trigger.bind(this),
  49. position: this.position,
  50. frontPosition: this.frontPosition,
  51. ui: this.ui,
  52. identifier: this.identifier,
  53. id: this.id,
  54. options: this.options
  55. };
  56. return this.instance;
  57. }
  58. Nipple.prototype = new Super();
  59. Nipple.constructor = Nipple;
  60. Nipple.id = 0;
  61. // Build the dom element of the Nipple instance.
  62. Nipple.prototype.buildEl = function (options) {
  63. this.ui = {};
  64. if (this.options.dataOnly) {
  65. return this;
  66. }
  67. this.ui.el = document.createElement('div');
  68. this.ui.back = document.createElement('div');
  69. this.ui.front = document.createElement('div');
  70. this.ui.el.className = 'nipple collection_' + this.collection.id;
  71. this.ui.back.className = 'back';
  72. this.ui.front.className = 'front';
  73. this.ui.el.setAttribute('id', 'nipple_' + this.collection.id +
  74. '_' + this.id);
  75. this.ui.el.appendChild(this.ui.back);
  76. this.ui.el.appendChild(this.ui.front);
  77. return this;
  78. };
  79. // Apply CSS to the Nipple instance.
  80. Nipple.prototype.stylize = function () {
  81. if (this.options.dataOnly) {
  82. return this;
  83. }
  84. var animTime = this.options.fadeTime + 'ms';
  85. var borderStyle = u.getVendorStyle('borderRadius', '50%');
  86. var transitStyle = u.getTransitionStyle('transition', 'opacity', animTime);
  87. var styles = {};
  88. styles.el = {
  89. position: 'absolute',
  90. opacity: this.options.restOpacity,
  91. display: 'block',
  92. 'zIndex': 999
  93. };
  94. styles.back = {
  95. position: 'absolute',
  96. display: 'block',
  97. width: this.options.size + 'px',
  98. height: this.options.size + 'px',
  99. marginLeft: -this.options.size / 2 + 'px',
  100. marginTop: -this.options.size / 2 + 'px',
  101. background: this.options.color,
  102. 'opacity': '.5'
  103. };
  104. styles.front = {
  105. width: this.options.size / 2 + 'px',
  106. height: this.options.size / 2 + 'px',
  107. position: 'absolute',
  108. display: 'block',
  109. marginLeft: -this.options.size / 4 + 'px',
  110. marginTop: -this.options.size / 4 + 'px',
  111. background: this.options.color,
  112. 'opacity': '.5'
  113. };
  114. u.extend(styles.el, transitStyle);
  115. if(this.options.shape === 'circle'){
  116. u.extend(styles.back, borderStyle);
  117. }
  118. u.extend(styles.front, borderStyle);
  119. this.applyStyles(styles);
  120. return this;
  121. };
  122. Nipple.prototype.applyStyles = function (styles) {
  123. // Apply styles
  124. for (var i in this.ui) {
  125. if (this.ui.hasOwnProperty(i)) {
  126. for (var j in styles[i]) {
  127. this.ui[i].style[j] = styles[i][j];
  128. }
  129. }
  130. }
  131. return this;
  132. };
  133. // Inject the Nipple instance into DOM.
  134. Nipple.prototype.addToDom = function () {
  135. // We're not adding it if we're dataOnly or already in dom.
  136. if (this.options.dataOnly || document.body.contains(this.ui.el)) {
  137. return this;
  138. }
  139. this.options.zone.appendChild(this.ui.el);
  140. return this;
  141. };
  142. // Remove the Nipple instance from DOM.
  143. Nipple.prototype.removeFromDom = function () {
  144. if (this.options.dataOnly || !document.body.contains(this.ui.el)) {
  145. return this;
  146. }
  147. this.options.zone.removeChild(this.ui.el);
  148. return this;
  149. };
  150. // Entirely destroy this nipple
  151. Nipple.prototype.destroy = function () {
  152. clearTimeout(this.removeTimeout);
  153. clearTimeout(this.showTimeout);
  154. clearTimeout(this.restTimeout);
  155. this.trigger('destroyed', this.instance);
  156. this.removeFromDom();
  157. this.off();
  158. };
  159. // Fade in the Nipple instance.
  160. Nipple.prototype.show = function (cb) {
  161. var self = this;
  162. if (self.options.dataOnly) {
  163. return self;
  164. }
  165. clearTimeout(self.removeTimeout);
  166. clearTimeout(self.showTimeout);
  167. clearTimeout(self.restTimeout);
  168. self.addToDom();
  169. self.restCallback();
  170. setTimeout(function () {
  171. self.ui.el.style.opacity = 1;
  172. }, 0);
  173. self.showTimeout = setTimeout(function () {
  174. self.trigger('shown', self.instance);
  175. if (typeof cb === 'function') {
  176. cb.call(this);
  177. }
  178. }, self.options.fadeTime);
  179. return self;
  180. };
  181. // Fade out the Nipple instance.
  182. Nipple.prototype.hide = function (cb) {
  183. var self = this;
  184. if (self.options.dataOnly) {
  185. return self;
  186. }
  187. self.ui.el.style.opacity = self.options.restOpacity;
  188. clearTimeout(self.removeTimeout);
  189. clearTimeout(self.showTimeout);
  190. clearTimeout(self.restTimeout);
  191. self.removeTimeout = setTimeout(
  192. function () {
  193. var display = self.options.mode === 'dynamic' ? 'none' : 'block';
  194. self.ui.el.style.display = display;
  195. if (typeof cb === 'function') {
  196. cb.call(self);
  197. }
  198. self.trigger('hidden', self.instance);
  199. },
  200. self.options.fadeTime
  201. );
  202. if (self.options.restJoystick) {
  203. const rest = self.options.restJoystick;
  204. const newPosition = {};
  205. newPosition.x = rest === true || rest.x !== false ? 0 : self.instance.frontPosition.x;
  206. newPosition.y = rest === true || rest.y !== false ? 0 : self.instance.frontPosition.y;
  207. self.setPosition(cb, newPosition);
  208. }
  209. return self;
  210. };
  211. // Set the nipple to the specified position
  212. Nipple.prototype.setPosition = function (cb, position) {
  213. var self = this;
  214. self.frontPosition = {
  215. x: position.x,
  216. y: position.y
  217. };
  218. var animTime = self.options.fadeTime + 'ms';
  219. var transitStyle = {};
  220. transitStyle.front = u.getTransitionStyle('transition',
  221. ['top', 'left'], animTime);
  222. var styles = {front: {}};
  223. styles.front = {
  224. left: self.frontPosition.x + 'px',
  225. top: self.frontPosition.y + 'px'
  226. };
  227. self.applyStyles(transitStyle);
  228. self.applyStyles(styles);
  229. self.restTimeout = setTimeout(
  230. function () {
  231. if (typeof cb === 'function') {
  232. cb.call(self);
  233. }
  234. self.restCallback();
  235. },
  236. self.options.fadeTime
  237. );
  238. };
  239. Nipple.prototype.restCallback = function () {
  240. var self = this;
  241. var transitStyle = {};
  242. transitStyle.front = u.getTransitionStyle('transition', 'none', '');
  243. self.applyStyles(transitStyle);
  244. self.trigger('rested', self.instance);
  245. };
  246. Nipple.prototype.resetDirection = function () {
  247. // Fully rebuild the object to let the iteration possible.
  248. this.direction = {
  249. x: false,
  250. y: false,
  251. angle: false
  252. };
  253. };
  254. Nipple.prototype.computeDirection = function (obj) {
  255. var rAngle = obj.angle.radian;
  256. var angle45 = Math.PI / 4;
  257. var angle90 = Math.PI / 2;
  258. var direction, directionX, directionY;
  259. // Angular direction
  260. // \ UP /
  261. // \ /
  262. // LEFT RIGHT
  263. // / \
  264. // /DOWN \
  265. //
  266. if (
  267. rAngle > angle45 &&
  268. rAngle < (angle45 * 3) &&
  269. !obj.lockX
  270. ) {
  271. direction = 'up';
  272. } else if (
  273. rAngle > -angle45 &&
  274. rAngle <= angle45 &&
  275. !obj.lockY
  276. ) {
  277. direction = 'left';
  278. } else if (
  279. rAngle > (-angle45 * 3) &&
  280. rAngle <= -angle45 &&
  281. !obj.lockX
  282. ) {
  283. direction = 'down';
  284. } else if (!obj.lockY) {
  285. direction = 'right';
  286. }
  287. // Plain direction
  288. // UP |
  289. // _______ | RIGHT
  290. // LEFT |
  291. // DOWN |
  292. if (!obj.lockY) {
  293. if (rAngle > -angle90 && rAngle < angle90) {
  294. directionX = 'left';
  295. } else {
  296. directionX = 'right';
  297. }
  298. }
  299. if (!obj.lockX) {
  300. if (rAngle > 0) {
  301. directionY = 'up';
  302. } else {
  303. directionY = 'down';
  304. }
  305. }
  306. if (obj.force > this.options.threshold) {
  307. var oldDirection = {};
  308. var i;
  309. for (i in this.direction) {
  310. if (this.direction.hasOwnProperty(i)) {
  311. oldDirection[i] = this.direction[i];
  312. }
  313. }
  314. var same = {};
  315. this.direction = {
  316. x: directionX,
  317. y: directionY,
  318. angle: direction
  319. };
  320. obj.direction = this.direction;
  321. for (i in oldDirection) {
  322. if (oldDirection[i] === this.direction[i]) {
  323. same[i] = true;
  324. }
  325. }
  326. // If all 3 directions are the same, we don't trigger anything.
  327. if (same.x && same.y && same.angle) {
  328. return obj;
  329. }
  330. if (!same.x || !same.y) {
  331. this.trigger('plain', obj);
  332. }
  333. if (!same.x) {
  334. this.trigger('plain:' + directionX, obj);
  335. }
  336. if (!same.y) {
  337. this.trigger('plain:' + directionY, obj);
  338. }
  339. if (!same.angle) {
  340. this.trigger('dir dir:' + direction, obj);
  341. }
  342. } else {
  343. this.resetDirection();
  344. }
  345. return obj;
  346. };
  347. export default Nipple;