From d5119de757f423f1198819e2f7515368bded03e4 Mon Sep 17 00:00:00 2001 From: Ivan Hayes Date: Fri, 21 Oct 2016 18:39:32 +0100 Subject: [PATCH] Simplify project structure --- .gitignore | 3 - .npmignore | 8 +- Gruntfile.js | 66 +++--- {js/libs => dist}/swiftclick.js | 0 {js/dist => dist}/swiftclick.min.js | 2 +- {css => example/css}/app.css | 0 index.html => example/index.html | 0 {js => example/js}/app/app.js | 0 example/js/libs/swiftclick.js | 306 ++++++++++++++++++++++++++++ package.json | 13 +- src/swiftclick.js | 306 ++++++++++++++++++++++++++++ 11 files changed, 652 insertions(+), 52 deletions(-) rename {js/libs => dist}/swiftclick.js (100%) rename {js/dist => dist}/swiftclick.min.js (56%) rename {css => example/css}/app.css (100%) rename index.html => example/index.html (100%) rename {js => example/js}/app/app.js (100%) create mode 100644 example/js/libs/swiftclick.js create mode 100644 src/swiftclick.js diff --git a/.gitignore b/.gitignore index 6f4a2a8..0797e03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ node_modules/ -tests/ npm-debug.log -# OS generated files # -###################### .DS_Store .DS_Store? ._* diff --git a/.npmignore b/.npmignore index 858add7..27ff318 100644 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,5 @@ .npmignore -css -js/app -js/dist +example/ +src/ bower.json -Gruntfile.js -index.html \ No newline at end of file +Gruntfile.js \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 7f430a0..07c0331 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,58 +2,50 @@ module.exports = function (grunt) { 'use strict'; - var sourceDir = 'js/'; - var distDir = 'js/dist/'; + var jsSrcDir = 'src/'; + var jsSrcFile = 'swiftclick.js'; + var jsDistDir = 'dist/'; var jsDistFile = 'swiftclick.min.js'; + var jsExampleLibsDir = 'example/js/libs/'; - var jsFilesArray = [sourceDir + 'libs/swiftclick.js']; - - // ==================== - // ==================== - - // Project configuration. grunt.initConfig({ watch: { - js: { - files: [ - 'Gruntfile.js', - 'js/app/app.js', - 'js/libs/swiftclick.js' - ], - - tasks: ['uglify:deploy'] + files: [jsSrcDir + jsSrcFile], + tasks: ['dist'] } }, uglify: { - - deploy: { - options: { - - compress: true, - - // mangle: Turn on or off mangling - mangle: true, - - // beautify: beautify your code for debugging/troubleshooting purposes - beautify: false, - - // report: Show file size report - report: 'gzip', + options: { + compress: { + drop_console: true }, + mangle: true, + beautify: false, + preserveComments: false + }, + dist: { + src: jsSrcDir + jsSrcFile, + dest: jsDistDir + jsDistFile + } + }, - src: jsFilesArray, - dest: distDir + jsDistFile + copy : { + dist : { + src : jsSrcDir + jsSrcFile, + dest : jsDistDir + jsSrcFile // copy with unminified file name + }, + example : { + src : jsSrcDir + jsSrcFile, + dest : jsExampleLibsDir + jsSrcFile // copy with unminified file name } } }); - grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-contrib-watch'); + require('load-grunt-tasks')(grunt, {pattern: ['grunt-*']}); - // A task for deployment - grunt.registerTask('deploy', ['uglify:deploy']); - grunt.registerTask('default', ['deploy']); + grunt.registerTask('default', ['uglify:dist', 'copy', 'watch']); + grunt.registerTask('dist', ['uglify:dist', 'copy']); }; diff --git a/js/libs/swiftclick.js b/dist/swiftclick.js similarity index 100% rename from js/libs/swiftclick.js rename to dist/swiftclick.js diff --git a/js/dist/swiftclick.min.js b/dist/swiftclick.min.js similarity index 56% rename from js/dist/swiftclick.min.js rename to dist/swiftclick.min.js index 86fa05f..3467ac1 100644 --- a/js/dist/swiftclick.min.js +++ b/dist/swiftclick.min.js @@ -1 +1 @@ -"use strict";function SwiftClick(a){function b(){"function"==typeof n&&(m.addEventListener("click",c,!1),m.onclick=null),m.addEventListener("touchstart",d,!1),m.addEventListener("click",f,!0)}function c(a){n(a)}function d(a){var b=a.target,c=b.nodeName.toLowerCase(),d=a.changedTouches[0];return"undefined"==typeof l.options.elements[c]?!0:o?!0:l.options.useCssParser&&i(b)?(r=!1,!0):(a.stopPropagation(),o=!0,p.x=d.pageX,p.y=d.pageY,q=h(),b.removeEventListener("touchend",e,!1),void b.addEventListener("touchend",e,!1))}function e(a){var b=a.target,c=a.changedTouches[0];return b.removeEventListener("touchend",e,!1),o=!1,j(c)?!0:(a.stopPropagation(),a.preventDefault(),r=!1,b.focus(),g(b,c),!1)}function f(a){var b=a.target,c=b.nodeName.toLowerCase();if("undefined"!=typeof l.options.elements[c]){if(r)return r=!1,a.stopPropagation(),a.preventDefault(),!1;r=!0}}function g(a,b){var c=document.createEvent("MouseEvents");c.initMouseEvent("click",!0,!0,window,1,b.screenX,b.screenY,b.clientX,b.clientY,!1,!1,!1,!1,0,null),a.dispatchEvent(c)}function h(){var a={x:window.pageXOffset||document.body.scrollLeft||document.documentElement.scrollLeft||0,y:window.pageYOffset||document.body.scrollTop||document.documentElement.scrollTop||0};return a}function i(a){var b="swiftclick-ignore",c="swiftclick-force",d=a.parentNode,e=!1;if(k(a,b))return!0;if(k(a,c))return e;if(null===d)return e;for(;d;)k(d,b)?(d=null,e=!0):d=d.parentNode;return e}function j(a){var b=l.options.maxTouchDrift,c=h();return Math.abs(a.pageX-p.x)>b||Math.abs(a.pageY-p.y)>b||Math.abs(c.x-q.x)>b||Math.abs(c.y-q.y)>b}function k(a,b){var c="undefined"!=typeof a.className?(" "+a.className+" ").indexOf(" "+b+" ")>-1:!1;return c}if("undefined"!=typeof SwiftClick.swiftDictionary[a])return SwiftClick.swiftDictionary[a];SwiftClick.swiftDictionary[a]=this,this.options={elements:{a:"a",div:"div",span:"span",button:"button"},minTouchDrift:4,maxTouchDrift:16,useCssParser:!1};var l=this,m=a,n=m.onclick,o=!1,p={x:0,y:0},q={x:0,y:0},r=!1;"onorientationchange"in window&&"ontouchstart"in window&&b()}SwiftClick.swiftDictionary={},SwiftClick.prototype.setMaxTouchDrift=function(a){if("number"!=typeof a)throw new TypeError('expected "maxTouchDrift" to be of type "number"');ac;c++){if("string"!=typeof a[c])throw new TypeError('all values within the "nodeNames" array must be of type "string"');b=a[c].toLowerCase(),this.options.elements[b]=b}},SwiftClick.prototype.replaceNodeNamesToTrack=function(a){this.options.elements={},this.addNodeNamesToTrack(a)},SwiftClick.prototype.useCssParser=function(a){this.options.useCssParser=a},SwiftClick.attach=function(a){return"undefined"!=typeof SwiftClick.swiftDictionary[a]?SwiftClick.swiftDictionary[a]:new SwiftClick(a)},"undefined"!=typeof define&&define.amd?define(function(){return SwiftClick}):"undefined"!=typeof module&&module.exports?module.exports=SwiftClick:window.SwiftClick=SwiftClick; \ No newline at end of file +"use strict";function SwiftClick(a){function b(){"function"==typeof n&&(m.addEventListener("click",c,!1),m.onclick=null),m.addEventListener("touchstart",d,!1),m.addEventListener("click",f,!0)}function c(a){n(a)}function d(a){var b=a.target,c=b.nodeName.toLowerCase(),d=a.changedTouches[0];return"undefined"==typeof l.options.elements[c]||(!!o||(l.options.useCssParser&&i(b)?(r=!1,!0):(a.stopPropagation(),o=!0,p.x=d.pageX,p.y=d.pageY,q=h(),b.removeEventListener("touchend",e,!1),void b.addEventListener("touchend",e,!1))))}function e(a){var b=a.target,c=a.changedTouches[0];return b.removeEventListener("touchend",e,!1),o=!1,!!j(c)||(a.stopPropagation(),a.preventDefault(),r=!1,b.focus(),g(b,c),!1)}function f(a){var b=a.target,c=b.nodeName.toLowerCase();if("undefined"!=typeof l.options.elements[c]){if(r)return r=!1,a.stopPropagation(),a.preventDefault(),!1;r=!0}}function g(a,b){var c=document.createEvent("MouseEvents");c.initMouseEvent("click",!0,!0,window,1,b.screenX,b.screenY,b.clientX,b.clientY,!1,!1,!1,!1,0,null),a.dispatchEvent(c)}function h(){var a={x:window.pageXOffset||document.body.scrollLeft||document.documentElement.scrollLeft||0,y:window.pageYOffset||document.body.scrollTop||document.documentElement.scrollTop||0};return a}function i(a){var b="swiftclick-ignore",c="swiftclick-force",d=a.parentNode,e=!1;if(k(a,b))return!0;if(k(a,c))return e;if(null===d)return e;for(;d;)k(d,b)?(d=null,e=!0):d=d.parentNode;return e}function j(a){var b=l.options.maxTouchDrift,c=h();return Math.abs(a.pageX-p.x)>b||Math.abs(a.pageY-p.y)>b||Math.abs(c.x-q.x)>b||Math.abs(c.y-q.y)>b}function k(a,b){var c="undefined"!=typeof a.className&&(" "+a.className+" ").indexOf(" "+b+" ")>-1;return c}if("undefined"!=typeof SwiftClick.swiftDictionary[a])return SwiftClick.swiftDictionary[a];SwiftClick.swiftDictionary[a]=this,this.options={elements:{a:"a",div:"div",span:"span",button:"button"},minTouchDrift:4,maxTouchDrift:16,useCssParser:!1};var l=this,m=a,n=m.onclick,o=!1,p={x:0,y:0},q={x:0,y:0},r=!1;"onorientationchange"in window&&"ontouchstart"in window&&b()}SwiftClick.swiftDictionary={},SwiftClick.prototype.setMaxTouchDrift=function(a){if("number"!=typeof a)throw new TypeError('expected "maxTouchDrift" to be of type "number"');a maxDrift || + Math.abs(touchend.pageY - _touchStartPoint.y) > maxDrift || + Math.abs(scrollPoint.x - _scrollStartPoint.x) > maxDrift || + Math.abs(scrollPoint.y - _scrollStartPoint.y) > maxDrift; + } + + function hasClass (el, className) { + + var classExists = typeof el.className !== 'undefined' ? (' ' + el.className + ' ').indexOf(' ' + className + ' ') > -1 : false; + + return classExists; + } +} + +SwiftClick.swiftDictionary = {}; + +SwiftClick.prototype.setMaxTouchDrift = function (maxTouchDrift) +{ + if (typeof maxTouchDrift !== 'number') + { + throw new TypeError ('expected "maxTouchDrift" to be of type "number"'); + } + + if (maxTouchDrift < this.options.minTouchDrift) + { + maxTouchDrift = this.options.minTouchDrift; + } + + this.options.maxTouchDrift = maxTouchDrift; +}; + +// add an array of node names (strings) for which swift clicks should be synthesized. +SwiftClick.prototype.addNodeNamesToTrack = function (nodeNamesArray) +{ + var i = 0; + var length = nodeNamesArray.length; + var currentNodeName; + + for (i; i < length; i++) + { + if (typeof nodeNamesArray[i] !== 'string') + { + throw new TypeError ('all values within the "nodeNames" array must be of type "string"'); + } + + currentNodeName = nodeNamesArray[i].toLowerCase(); + this.options.elements[currentNodeName] = currentNodeName; + } +}; + +SwiftClick.prototype.replaceNodeNamesToTrack = function (nodeNamesArray) +{ + this.options.elements = {}; + this.addNodeNamesToTrack(nodeNamesArray); +}; + +SwiftClick.prototype.useCssParser = function (useParser) +{ + this.options.useCssParser = useParser; +}; + +// use a basic implementation of the composition pattern in order to create new instances of SwiftClick. +SwiftClick.attach = function (contextEl) +{ + // if SwiftClick has already been initialised on this element then return the instance that's already in the Dictionary. + if (typeof SwiftClick.swiftDictionary[contextEl] !== 'undefined') + { + return SwiftClick.swiftDictionary[contextEl]; + } + + return new SwiftClick(contextEl); +}; + + +// check for AMD/Module support, otherwise define SwiftClick as a global variable. +if (typeof define !== 'undefined' && define.amd) +{ + // AMD. Register as an anonymous module. + define (function() + { + return SwiftClick; + }); + +} +else if (typeof module !== 'undefined' && module.exports) +{ + module.exports = SwiftClick; +} +else +{ + window.SwiftClick = SwiftClick; +} \ No newline at end of file diff --git a/package.json b/package.json index ede94b0..2447509 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,13 @@ "name": "swiftclick", "title": "SwiftClick", "description": "Eliminates the 300ms click event delay on touch devices that support orientation change", - "version": "1.4.1", + "version": "2.0.0", "homepage": "https://github.com/munkychop/swiftclick", "author": { "name": "Ivan Hayes", "url": "http://www.ivanhayes.com" }, - "main": "js/libs/swiftclick.js", + "main": "dist/swiftclick.js", "repository": { "type": "git", "url": "git://github.com/munkychop/swiftclick.git" @@ -24,10 +24,11 @@ ], "private": false, "license": "MIT", - "dependencies": {}, "devDependencies": { - "grunt": "^0.4.5", - "grunt-contrib-uglify": "^0.10.0", - "grunt-contrib-watch": "^0.6.1" + "grunt": "^1.0.1", + "grunt-contrib-copy": "^1.0.0", + "grunt-contrib-uglify": "^2.0.0", + "grunt-contrib-watch": "^1.0.0", + "load-grunt-tasks": "^3.5.2" } } diff --git a/src/swiftclick.js b/src/swiftclick.js new file mode 100644 index 0000000..160a738 --- /dev/null +++ b/src/swiftclick.js @@ -0,0 +1,306 @@ +/* + * @license MIT License (see license.txt) + */ + +'use strict'; + +function SwiftClick (contextEl) +{ + // if SwiftClick has already been initialised on this element then return the instance that's already in the Dictionary. + if (typeof SwiftClick.swiftDictionary[contextEl] !== 'undefined') return SwiftClick.swiftDictionary[contextEl]; + + // add this instance of SwiftClick to the dictionary using the contextEl as the key. + SwiftClick.swiftDictionary[contextEl] = this; + + this.options = + { + elements: {a:'a', div:'div', span:'span', button:'button'}, + minTouchDrift: 4, + maxTouchDrift: 16, + useCssParser: false + }; + + var _self = this; + var _swiftContextEl = contextEl; + var _swiftContextElOriginalClick = _swiftContextEl.onclick; + var _currentlyTrackingTouch = false; + var _touchStartPoint = {x:0, y:0}; + var _scrollStartPoint = {x:0, y:0}; + var _clickedAlready = false; + + + // SwiftClick is only initialised if both touch and orientationchange are supported. + if ('onorientationchange' in window && 'ontouchstart' in window) + { + init(); + } + + function init () + { + // check if the swift el already has a click handler and if so hijack it so it get's fired after SwiftClick's, instead of beforehand. + if (typeof _swiftContextElOriginalClick === 'function') + { + _swiftContextEl.addEventListener('click', hijackedSwiftElClickHandler, false); + _swiftContextEl.onclick = null; + } + + _swiftContextEl.addEventListener('touchstart', touchStartHandler, false); + _swiftContextEl.addEventListener('click', clickHandler, true); + } + + function hijackedSwiftElClickHandler (event) + { + _swiftContextElOriginalClick(event); + } + + function touchStartHandler (event) + { + var targetEl = event.target; + var nodeName = targetEl.nodeName.toLowerCase(); + var touch = event.changedTouches[0]; + + // don't synthesize an event if the node is not an acceptable type (the type isn't in the dictionary). + if (typeof _self.options.elements[nodeName] === 'undefined') + { + return true; + } + + // don't synthesize an event if we are already tracking an element. + if (_currentlyTrackingTouch) + { + return true; + } + + // check parents for 'swiftclick-ignore' class name. + if (_self.options.useCssParser && checkIfElementShouldBeIgnored(targetEl)) + { + _clickedAlready = false; + return true; + } + + event.stopPropagation(); + + _currentlyTrackingTouch = true; + + // store touchstart positions so we can check for changes later (within touchend handler). + _touchStartPoint.x = touch.pageX; + _touchStartPoint.y = touch.pageY; + _scrollStartPoint = getScrollPoint(); + + // only add the 'touchend' listener now that we know the element should be tracked. + targetEl.removeEventListener('touchend', touchEndHandler, false); + targetEl.addEventListener('touchend', touchEndHandler, false); + } + + function touchEndHandler (event) + { + var targetEl = event.target; + var touchend = event.changedTouches[0]; + + targetEl.removeEventListener('touchend', touchEndHandler, false); + + _currentlyTrackingTouch = false; + + // don't synthesize a click event if the touchpoint position has drifted significantly, as the user is not trying to click. + if (hasTouchDriftedTooFar(touchend)) + { + return true; + } + + // prevent default actions and create a synthetic click event before returning false. + event.stopPropagation(); + event.preventDefault(); + + _clickedAlready = false; + + targetEl.focus(); + synthesizeClickEvent(targetEl, touchend); + + // return false in order to surpress the regular click event. + return false; + } + + function clickHandler (event) + { + var targetEl = event.target; + var nodeName = targetEl.nodeName.toLowerCase(); + + if (typeof _self.options.elements[nodeName] !== 'undefined') + { + if (_clickedAlready) + { + _clickedAlready = false; + + event.stopPropagation(); + event.preventDefault(); + return false; + } + + _clickedAlready = true; + } + } + + function synthesizeClickEvent (el, touchend) + { + var clickEvent = document.createEvent('MouseEvents'); + clickEvent.initMouseEvent('click', true, true, window, 1, touchend.screenX, touchend.screenY, touchend.clientX, touchend.clientY, false, false, false, false, 0, null); + + el.dispatchEvent(clickEvent); + } + + function getScrollPoint () + { + var scrollPoint = + { + x : window.pageXOffset || + document.body.scrollLeft || + document.documentElement.scrollLeft || + 0, + y : window.pageYOffset || + document.body.scrollTop || + document.documentElement.scrollTop || + 0 + }; + + return scrollPoint; + } + + function checkIfElementShouldBeIgnored (el) + { + var classToIgnore = 'swiftclick-ignore'; + var classToForceClick = 'swiftclick-force'; + var parentEl = el.parentNode; + var shouldIgnoreElement = false; + + // ignore the target el and return early if it has the 'swiftclick-ignore' class. + if (hasClass(el, classToIgnore)) + { + return true; + } + + // don't ignore the target el and return early if it has the 'swiftclick-force' class. + if (hasClass(el, classToForceClick)) + { + return shouldIgnoreElement; + } + + // the topmost element has been reached. + if (parentEl === null) + { + return shouldIgnoreElement; + } + + // ignore the target el if one of its parents has the 'swiftclick-ignore' class. + while (parentEl) + { + if (hasClass(parentEl, classToIgnore)) + { + parentEl = null; + shouldIgnoreElement = true; + } + else + { + parentEl = parentEl.parentNode; + } + } + + return shouldIgnoreElement; + } + + function hasTouchDriftedTooFar (touchend) + { + var maxDrift = _self.options.maxTouchDrift; + var scrollPoint = getScrollPoint(); + + return Math.abs(touchend.pageX - _touchStartPoint.x) > maxDrift || + Math.abs(touchend.pageY - _touchStartPoint.y) > maxDrift || + Math.abs(scrollPoint.x - _scrollStartPoint.x) > maxDrift || + Math.abs(scrollPoint.y - _scrollStartPoint.y) > maxDrift; + } + + function hasClass (el, className) { + + var classExists = typeof el.className !== 'undefined' ? (' ' + el.className + ' ').indexOf(' ' + className + ' ') > -1 : false; + + return classExists; + } +} + +SwiftClick.swiftDictionary = {}; + +SwiftClick.prototype.setMaxTouchDrift = function (maxTouchDrift) +{ + if (typeof maxTouchDrift !== 'number') + { + throw new TypeError ('expected "maxTouchDrift" to be of type "number"'); + } + + if (maxTouchDrift < this.options.minTouchDrift) + { + maxTouchDrift = this.options.minTouchDrift; + } + + this.options.maxTouchDrift = maxTouchDrift; +}; + +// add an array of node names (strings) for which swift clicks should be synthesized. +SwiftClick.prototype.addNodeNamesToTrack = function (nodeNamesArray) +{ + var i = 0; + var length = nodeNamesArray.length; + var currentNodeName; + + for (i; i < length; i++) + { + if (typeof nodeNamesArray[i] !== 'string') + { + throw new TypeError ('all values within the "nodeNames" array must be of type "string"'); + } + + currentNodeName = nodeNamesArray[i].toLowerCase(); + this.options.elements[currentNodeName] = currentNodeName; + } +}; + +SwiftClick.prototype.replaceNodeNamesToTrack = function (nodeNamesArray) +{ + this.options.elements = {}; + this.addNodeNamesToTrack(nodeNamesArray); +}; + +SwiftClick.prototype.useCssParser = function (useParser) +{ + this.options.useCssParser = useParser; +}; + +// use a basic implementation of the composition pattern in order to create new instances of SwiftClick. +SwiftClick.attach = function (contextEl) +{ + // if SwiftClick has already been initialised on this element then return the instance that's already in the Dictionary. + if (typeof SwiftClick.swiftDictionary[contextEl] !== 'undefined') + { + return SwiftClick.swiftDictionary[contextEl]; + } + + return new SwiftClick(contextEl); +}; + + +// check for AMD/Module support, otherwise define SwiftClick as a global variable. +if (typeof define !== 'undefined' && define.amd) +{ + // AMD. Register as an anonymous module. + define (function() + { + return SwiftClick; + }); + +} +else if (typeof module !== 'undefined' && module.exports) +{ + module.exports = SwiftClick; +} +else +{ + window.SwiftClick = SwiftClick; +} \ No newline at end of file