feat: Phase 2.5 - Variant Collections Spreadsheet Editor

Replaces JSON textarea with professional Excel-like spreadsheet interface for managing product variant properties.

Features:
- Handsontable 14.6.1 spreadsheet component
- Property presets (Size, Color, Material, Storage, Custom)
- Inline cell editing with Tab/Enter navigation
- Context menu for add/remove rows and columns
- Keyboard shortcuts (Ctrl+D delete, Ctrl+Enter save, Ctrl+Z undo)
- Mobile touch gestures (swipe to delete rows)
- Automatic JSON serialization on form submit
- Form validation before saving
- Comprehensive user guide documentation

Files Changed:
- LittleShop/package.json: NPM package management setup
- LittleShop/wwwroot/js/variant-editor.js: 400-line spreadsheet editor module
- LittleShop/wwwroot/lib/handsontable/: Handsontable library (Community Edition)
- LittleShop/wwwroot/lib/hammerjs/: Hammer.js touch gesture library
- LittleShop/Areas/Admin/Views/VariantCollections/Edit.cshtml: Spreadsheet UI integration
- VARIANT_COLLECTIONS_USER_GUIDE.md: Complete user guide (18+ pages)

Technical Details:
- Excel-like editing experience (no more manual JSON editing)
- Mobile-first responsive design
- Browser compatibility: Chrome 90+, Firefox 88+, Edge 90+, Safari 14+
- Touch-optimized for mobile administration
- Automatic data validation and error handling
This commit is contained in:
sysadmin
2025-11-13 19:40:06 +00:00
parent a272373246
commit 76efba55bd
125 changed files with 273259 additions and 6 deletions

View File

@@ -0,0 +1,42 @@
@import url(http://fonts.googleapis.com/css?family=Open+Sans);
*, *:after, *:before {
box-sizing: border-box;
-moz-box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
min-height: 100%;
background: #eee;
font: 13px/1.5em 'Open Sans', Helvetica, Arial, sans-serif;
}
a {
color: #4986e7;
}
.bg1, .green { background: #42d692; }
.bg2, .blue { background: #4986e7; }
.bg3, .red { background: #d06b64; }
.bg4, .purple { background: #cd74e6; }
.bg5, .azure { background: #9fe1e7; }
body {
margin: 20px;
}
pre {
background: #fff;
padding: 20px;
margin-bottom: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
.clear { clear: both; }

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no">
<title></title>
</head>
<body>
Open the inspector and play a bit with the touchAction property.
<script src="../../hammer.min.js"></script>
<script>
var mc = new Hammer(document.body);
mc.add(new Hammer.Swipe({ direction: Hammer.DIRECTION_HORIZONTAL }));
mc.add(new Hammer.Pan({ direction: Hammer.DIRECTION_HORIZONTAL }));
console.log(document.body.style.touchAction)
</script>
</body>
</html>

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<meta name="msapplication-tap-highlight" content="no"/>
<link rel="stylesheet" href="assets/style.css">
<title>Hammer.js</title>
</head>
<body>
<div class="container">
<div id="hit" class="bg1" style="padding: 30px; height: 200px;">
</div>
<pre id="debug" style="overflow:hidden; background: #eee; padding: 15px;"></pre>
<pre id="log" style="overflow:hidden;"></pre>
</div>
<script src="../../hammer.js"></script>
<script>
Object.prototype.toDirString = function() {
var output = [];
for(var key in this) {
if(this.hasOwnProperty(key)) {
var value = this[key];
if(Array.isArray(value)) {
value = "Array("+ value.length +"):"+ value;
} else if(value instanceof HTMLElement) {
value = value +" ("+ value.outerHTML.substring(0, 50) +"...)";
}
output.push(key +": "+ value);
}
}
return output.join("\n")
};
var el = document.querySelector("#hit");
var log = document.querySelector("#log");
var debug = document.querySelector("#debug");
var mc = new Hammer(el);
mc.get('pinch').set({ enable: true });
mc.on("hammer.input", function(ev) {
debug.innerHTML = [ev.srcEvent.type, ev.pointers.length, ev.isFinal, ev.deltaX, ev.deltaY].join("<br>");
});
</script>
</body>
</html>

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="assets/style.css">
<title>Hammer.js</title>
</head>
<body>
<div class="container">
<div id="hit" class="bg1" style="padding: 30px;">
<span id="target" class="bg5" style="display: block; height: 100px;"></span>
</div>
<pre id="debug" style="overflow:hidden; background: #eee; padding: 15px;"></pre>
<pre id="log" style="overflow:hidden;"></pre>
</div>
<script src="../../hammer.min.js"></script>
<script>
Object.prototype.toDirString = function() {
var output = [];
for(var key in this) {
if(this.hasOwnProperty(key)) {
var value = this[key];
if(Array.isArray(value)) {
value = "Array("+ value.length +"):"+ value;
} else if(value instanceof HTMLElement) {
value = value +" ("+ value.outerHTML.substring(0, 50) +"...)";
}
output.push(key +": "+ value);
}
}
return output.join("\n")
};
var el = document.querySelector("#hit");
var log = document.querySelector("#log");
var debug = document.querySelector("#debug");
var mc = new Hammer(el);
mc.get('pinch').set({ enable: true });
mc.get('rotate').set({ enable: true });
mc.on("swipe pan panstart panmove panend pancancel multipan press pressup pinch rotate tap doubletap",
logGesture);
function DEBUG(str) {
debug.textContent = str;
}
function logGesture(ev) {
console.log(ev.timeStamp, ev.type, ev);
log.textContent = ev.toDirString();
}
</script>
</body>
</html>

View File

@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="assets/style.css">
<title>Hammer.js</title>
<style>
#right,
#left {
display: block;
width: 50%;
height: 500px;
overflow: hidden;
}
#left { float: left; }
#right { float: right; }
</style>
</head>
<body>
<div class="container">
<pre id="left" class="bg1"></pre>
<pre id="right" class="bg5"></pre>
<div class="clear"></div>
<h1>Multiple instances the same time</h1>
<p>You can run multiple instances of Hammer on your page and they will recognize each completely isolated
from each other. This makes it possible to build multi-user interfaces.</p>
</div>
<script src="../../hammer.min.js"></script>
<script>
Object.prototype.toDirString = function() {
var output = [];
for(var key in this) {
if(this.hasOwnProperty(key)) {
var value = this[key];
if(Array.isArray(value)) {
value = "Array("+ value.length +"):"+ value;
} else if(value instanceof HTMLElement) {
value = value +" ("+ value.outerHTML.substring(0, 50) +"...)";
}
output.push(key +": "+ value);
}
}
return output.join("\n")
};
function addHammer(el) {
var mc = new Hammer(el, { multiUser: true });
mc.get('pan').set({ direction: Hammer.DIRECTION_ALL });
mc.get('swipe').set({ direction: Hammer.DIRECTION_ALL });
mc.get('pinch').set({ enable: true });
mc.get('rotate').set({ enable: true });
mc.on("swipe pan press pinch rotate tap doubletap", function (ev) {
ev.preventDefault();
el.innerText = ev.toDirString();
});
}
addHammer(document.querySelector("#left"));
addHammer(document.querySelector("#right"));
</script>
</body>
</html>

View File

@@ -0,0 +1,217 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<meta name="msapplication-tap-highlight" content="no"/>
<link rel="stylesheet" href="assets/style.css">
<title>Hammer.js</title>
<style>
.container {
max-width: 900px;
margin: 0 auto;
}
.panes.wrapper {
max-height: 400px;
max-width: 800px;
background: #666;
margin: 40px auto;
}
.panes {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
.pane {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
text-align: center;
font: bold 60px/250px 'Open Sans', Helvetica, Arial, sans-serif;
color: #fff;
}
.panes.animate > .pane {
transition: all .3s;
-webkit-transition: all .3s;
}
</style>
</head>
<body>
<div class="panes wrapper">
<div class="pane bg1">
<div class="panes">
<div class="pane" style="background: rgba(0,0,0,0);">1.1</div>
<div class="pane" style="background: rgba(0,0,0,.2);">1.2</div>
<div class="pane" style="background: rgba(0,0,0,.4);">1.3</div>
<div class="pane" style="background: rgba(0,0,0,.6);">1.4</div>
<div class="pane" style="background: rgba(0,0,0,.8);">1.5</div>
</div>
</div>
<div class="pane bg2">
<div class="panes">
<div class="pane" style="background: rgba(0,0,0,0);">2.1</div>
<div class="pane" style="background: rgba(0,0,0,.2);">2.2</div>
<div class="pane" style="background: rgba(0,0,0,.4);">2.3</div>
<div class="pane" style="background: rgba(0,0,0,.6);">2.4</div>
<div class="pane" style="background: rgba(0,0,0,.8);">2.5</div>
</div>
</div>
<div class="pane bg3">
<div class="panes">
<div class="pane" style="background: rgba(0,0,0,0);">3.1</div>
<div class="pane" style="background: rgba(0,0,0,.2);">3.2</div>
<div class="pane" style="background: rgba(0,0,0,.4);">3.3</div>
<div class="pane" style="background: rgba(0,0,0,.6);">3.4</div>
<div class="pane" style="background: rgba(0,0,0,.8);">3.5</div>
</div>
</div>
<div class="pane bg4">
<div class="panes">
<div class="pane" style="background: rgba(0,0,0,0);">4.1</div>
<div class="pane" style="background: rgba(0,0,0,.2);">4.2</div>
<div class="pane" style="background: rgba(0,0,0,.4);">4.3</div>
<div class="pane" style="background: rgba(0,0,0,.6);">4.4</div>
<div class="pane" style="background: rgba(0,0,0,.8);">4.5</div>
</div>
</div>
<div class="pane bg5">
<div class="panes">
<div class="pane" style="background: rgba(0,0,0,0);">5.1</div>
<div class="pane" style="background: rgba(0,0,0,.2);">5.2</div>
<div class="pane" style="background: rgba(0,0,0,.4);">5.3</div>
<div class="pane" style="background: rgba(0,0,0,.6);">5.4</div>
<div class="pane" style="background: rgba(0,0,0,.8);">5.5</div>
</div>
</div>
</div>
<div class="container">
<h1>Nested Pan recognizers</h1>
<p>Nested recognizers are possible with some threshold and with use of <code>requireFailure()</code>.</p>
</div>
<script src="../../hammer.js"></script>
<script>
var reqAnimationFrame = (function() {
return window[Hammer.prefixed(window, "requestAnimationFrame")] || function(callback) {
setTimeout(callback, 1000 / 60);
}
})();
function dirProp(direction, hProp, vProp) {
return (direction & Hammer.DIRECTION_HORIZONTAL) ? hProp : vProp
}
/**
* Carousel
* @param container
* @param direction
* @constructor
*/
function HammerCarousel(container, direction) {
this.container = container;
this.direction = direction;
this.panes = Array.prototype.slice.call(this.container.children, 0);
this.containerSize = this.container[dirProp(direction, 'offsetWidth', 'offsetHeight')];
this.currentIndex = 0;
this.hammer = new Hammer.Manager(this.container);
this.hammer.add(new Hammer.Pan({ direction: this.direction, threshold: 10 }));
this.hammer.on("panstart panmove panend pancancel", Hammer.bindFn(this.onPan, this));
this.show(this.currentIndex);
}
HammerCarousel.prototype = {
/**
* show a pane
* @param {Number} showIndex
* @param {Number} [percent] percentage visible
* @param {Boolean} [animate]
*/
show: function(showIndex, percent, animate){
showIndex = Math.max(0, Math.min(showIndex, this.panes.length - 1));
percent = percent || 0;
var className = this.container.className;
if(animate) {
if(className.indexOf('animate') === -1) {
this.container.className += ' animate';
}
} else {
if(className.indexOf('animate') !== -1) {
this.container.className = className.replace('animate', '').trim();
}
}
var paneIndex, pos, translate;
for (paneIndex = 0; paneIndex < this.panes.length; paneIndex++) {
pos = (this.containerSize / 100) * (((paneIndex - showIndex) * 100) + percent);
if(this.direction & Hammer.DIRECTION_HORIZONTAL) {
translate = 'translate3d(' + pos + 'px, 0, 0)';
} else {
translate = 'translate3d(0, ' + pos + 'px, 0)'
}
this.panes[paneIndex].style.transform = translate;
this.panes[paneIndex].style.mozTransform = translate;
this.panes[paneIndex].style.webkitTransform = translate;
}
this.currentIndex = showIndex;
},
/**
* handle pan
* @param {Object} ev
*/
onPan : function (ev) {
var delta = dirProp(this.direction, ev.deltaX, ev.deltaY);
var percent = (100 / this.containerSize) * delta;
var animate = false;
if (ev.type == 'panend' || ev.type == 'pancancel') {
if (Math.abs(percent) > 20 && ev.type == 'panend') {
this.currentIndex += (percent < 0) ? 1 : -1;
}
percent = 0;
animate = true;
}
this.show(this.currentIndex, percent, animate);
}
};
// the horizontal pane scroller
var outer = new HammerCarousel(document.querySelector(".panes.wrapper"), Hammer.DIRECTION_HORIZONTAL);
// each pane should contain a vertical pane scroller
Hammer.each(document.querySelectorAll(".pane .panes"), function(container) {
// setup the inner scroller
var inner = new HammerCarousel(container, Hammer.DIRECTION_VERTICAL);
// only recognize the inner pan when the outer is failing.
// they both have a threshold of some px
outer.hammer.get('pan').requireFailure(inner.hammer.get('pan'));
});
</script>
</body>
</html>

View File

@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="assets/style.css">
<title>Hammer.js</title>
</head>
<body>
<div class="container">
<div id="maps" style="height: 500px; margin-bottom: 20px;"></div>
<h1>Gestures simulator</h1>
<p>Used for unit-testing Hammer.js. To test it on the Google Maps view, you should open your
<a href="https://developer.chrome.com/devtools/docs/mobile-emulation#emulate-touch-events">
Inspector and emulate a touch-screen.</a>
Or just open it on your touch-device.</p>
<p>Currently, it only triggers touchEvents.</p>
</div>
<script src="../../node_modules/hammer-simulator/index.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?v=3.exp"></script>
<script>
Simulator.events.touch.fakeSupport();
var el, map;
var container = document.getElementById('maps');
var program = [
['pan', ['deltaX','deltaY']],
['pinch', ['scale']],
['tap', ['pos']],
['swipe', ['deltaX','deltaY']],
['pinch', ['pos','scale']],
['pan', ['pos']],
['rotate', ['pos','rotation']],
['doubleTap', ['pos']],
['pinchRotate', ['pos','scale','rotation']],
];
function randomRange(min, max) {
return Math.random() * (max - min) + min;
}
function randomRangeInt(min, max) {
return Math.round(randomRange(min, max));
}
function gestureOption(name) {
var max = map.getDiv().offsetWidth * .9;
switch(name) {
case 'deltaY':
case 'deltaX':
return randomRangeInt(10, max * .5);
case 'pos':
return [randomRangeInt(10, max), randomRangeInt(10, max)];
case 'scale':
return randomRange(.2, 2);
case 'rotation':
return randomRange(-180, 180);
}
}
function walkProgram(done) {
var clone = [].concat(program);
(function next() {
if(clone.length) {
var entry = clone.shift();
var options = {};
for(var i=0; i<entry[1].length; i++) {
options[entry[1][i]] = gestureOption(entry[1][i]);
}
Simulator.gestures[entry[0]](el, options, next);
} else {
done();
}
}());
}
function startSimulator() {
walkProgram(startSimulator);
}
(function setupGoogleMaps() {
map = new google.maps.Map(container, {
zoom: 14,
center: new google.maps.LatLng(51.98, 5.91)
});
// collect the target element
google.maps.event.addListenerOnce(map, 'idle', function(){
el = container.childNodes[0].childNodes[0];
startSimulator();
});
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="assets/style.css">
<title>Hammer.js</title>
</head>
<body>
<div class="container">
<div id="hit" class="bg4" style="padding: 30px; height: 300px; position: relative;">
</div>
<pre id="debug" style="overflow:hidden; background: #eee; padding: 15px;"></pre>
<pre id="log" style="overflow:hidden;"></pre>
</div>
<script src="../../node_modules/hammer-simulator/index.js"></script>
<script src="../../hammer.js"></script>
<script>
var program = [
['pan', ['deltaX','deltaY']],
['pinch', ['scale']],
['tap', ['pos']],
['swipe', ['deltaX','deltaY']],
['pinch', ['pos','scale']],
['pan', ['pos']],
['rotate', ['pos','rotation']],
['doubleTap', ['pos']],
['pinchRotate', ['pos','scale','rotation']],
];
function randomRange(min, max) {
return Math.random() * (max - min) + min;
}
function randomRangeInt(min, max) {
return Math.round(randomRange(min, max));
}
function gestureOption(name) {
var max = el.offsetWidth * .9;
switch(name) {
case 'deltaY':
case 'deltaX':
return randomRangeInt(10, max * .5);
case 'pos':
return [randomRangeInt(10, max), randomRangeInt(10, max)];
case 'scale':
return randomRange(.2, 2);
case 'rotation':
return randomRange(-180, 180);
}
}
function walkProgram(done) {
var clone = [].concat(program);
(function next() {
if(clone.length) {
var entry = clone.shift();
var options = {};
for(var i=0; i<entry[1].length; i++) {
options[entry[1][i]] = gestureOption(entry[1][i]);
}
setTimeout(function() {
log.innerHTML = '';
Simulator.gestures[entry[0]](el, options, next);
}, 1000);
} else {
done();
}
}());
}
function startSimulator() {
walkProgram(startSimulator);
}
var el = document.querySelector("#hit");
var log = document.querySelector("#log");
var debug = document.querySelector("#debug");
var mc = new Hammer(el);
mc.get('pinch').set({ enable: true, threshold:.1 });
mc.get('rotate').set({ enable: true });
mc.on("swipe pan multipan press pinch rotate tap doubletap", logGesture);
function logGesture(ev) {
log.textContent = ev.toDirString();
}
Object.prototype.toDirString = function() {
var output = [];
for(var key in this) {
if(this.hasOwnProperty(key)) {
var value = this[key];
if(Array.isArray(value)) {
value = "Array("+ value.length +"):"+ value;
} else if(value instanceof HTMLElement) {
value = value +" ("+ value.outerHTML.substring(0, 50) +"...)";
}
output.push(key +": "+ value);
}
}
return output.join("\n")
};
startSimulator();
</script>
</body>
</html>

View File

@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="assets/style.css">
<title>Hammer.js</title>
<style>
.tester {
margin: 20px 0;
padding: 10px;
height: 200px;
overflow: hidden;
}
.scroll-space {
height: 9000px;
}
#native, #no-native {
color: #fff;
font-weight: bold;
font-size: 1.1em;
padding: 10px 20px;
display: none;
margin: 25px 0;
}
.show {
display: block !important;
}
</style>
</head>
<body>
<div class="container">
<p>Hammer provides a <a href="../../src/touchaction.js">kind of polyfill</a>
for the browsers that don't support the <a href="http://www.w3.org/TR/pointerevents/#the-touch-action-css-property">touch-action</a> property.</p>
<div id="native" class="green">Your browser has support for the touch-action property!</div>
<div id="no-native" class="red">Your browser doesn't support the touch-action property,
so we use the polyfill.</div>
<h2>touch-action: auto</h2>
<p>Should prevent nothing.</p>
<div class="tester azure" id="auto"></div>
<h2>touch-action: pan-y</h2>
<p>Should prevent scrolling on horizontal movement. This is set by default when creating a Hammer instance.</p>
<div class="tester azure" id="pan-y"></div>
<h2>touch-action: pan-x</h2>
<p>Should prevent scrolling on vertical movement.</p>
<div class="tester azure" id="pan-x"></div>
<h2>touch-action: pan-x pan-y</h2>
<p>Should <strong>not</strong> prevent any scrolling on any movement. Horizontal and vertical scrolling handled by the browser directly.</p>
<div class="tester azure" id="pan-x-pan-y"></div>
<h2>touch-action: none</h2>
<p>Should prevent all.</p>
<div class="tester azure" id="none"></div>
</div>
<script src="../../hammer.js"></script>
<script>
var support = Hammer.prefixed(document.body.style, 'touchAction');
document.getElementById(support ? 'native' : 'no-native').className += ' show';
var touchActions = ['auto', 'pan-y', 'pan-x', 'pan-x pan-y', 'none'];
Hammer.each(touchActions, function(touchAction) {
var el = document.getElementById(touchAction.replace(" ", "-"));
var mc = Hammer(el, {
touchAction: touchAction
});
mc.get('pan').set({ direction: Hammer.DIRECTION_ALL });
mc.get('pinch').set({ enable: true });
mc.get('rotate').set({ enable: true });
mc.on("pan swipe rotate pinch tap doubletap press", function(ev) {
el.textContent = ev.type +" "+ el.textContent;
});
});
</script>
<div class="scroll-space"></div>
<p>hi.</p>
</body>
</html>

View File

@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="assets/style.css">
<title>Hammer.js</title>
<style>
html, body {
overflow: hidden;
margin: 0;
}
body {
-webkit-perspective: 500;
-moz-perspective: 500;
perspective: 500;
}
.animate {
-webkit-transition: all .3s;
-moz-transition: all .3s;
transition: all .3s;
}
#hit {
padding: 10px;
}
#log {
position: absolute;
padding: 10px;
}
</style>
</head>
<body>
<div id="log"></div>
<div id="hit" style="background: #42d692; width: 150px; height: 150px;"></div>
<script src="../../hammer.js"></script>
<script>
var reqAnimationFrame = (function () {
return window[Hammer.prefixed(window, 'requestAnimationFrame')] || function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
var log = document.querySelector("#log");
var el = document.querySelector("#hit");
var START_X = Math.round((window.innerWidth - el.offsetWidth) / 2);
var START_Y = Math.round((window.innerHeight - el.offsetHeight) / 2);
var ticking = false;
var transform;
var timer;
var mc = new Hammer.Manager(el);
mc.add(new Hammer.Pan({ threshold: 0, pointers: 0 }));
mc.add(new Hammer.Swipe()).recognizeWith(mc.get('pan'));
mc.add(new Hammer.Rotate({ threshold: 0 })).recognizeWith(mc.get('pan'));
mc.add(new Hammer.Pinch({ threshold: 0 })).recognizeWith([mc.get('pan'), mc.get('rotate')]);
mc.add(new Hammer.Tap({ event: 'doubletap', taps: 2 }));
mc.add(new Hammer.Tap());
mc.on("panstart panmove", onPan);
mc.on("rotatestart rotatemove", onRotate);
mc.on("pinchstart pinchmove", onPinch);
mc.on("swipe", onSwipe);
mc.on("tap", onTap);
mc.on("doubletap", onDoubleTap);
mc.on("hammer.input", function(ev) {
if(ev.isFinal) {
resetElement();
}
});
function resetElement() {
el.className = 'animate';
transform = {
translate: { x: START_X, y: START_Y },
scale: 1,
angle: 0,
rx: 0,
ry: 0,
rz: 0
};
requestElementUpdate();
if (log.textContent.length > 2000) {
log.textContent = log.textContent.substring(0, 2000) + "...";
}
}
function updateElementTransform() {
var value = [
'translate3d(' + transform.translate.x + 'px, ' + transform.translate.y + 'px, 0)',
'scale(' + transform.scale + ', ' + transform.scale + ')',
'rotate3d('+ transform.rx +','+ transform.ry +','+ transform.rz +','+ transform.angle + 'deg)'
];
value = value.join(" ");
el.textContent = value;
el.style.webkitTransform = value;
el.style.mozTransform = value;
el.style.transform = value;
ticking = false;
}
function requestElementUpdate() {
if(!ticking) {
reqAnimationFrame(updateElementTransform);
ticking = true;
}
}
function logEvent(str) {
//log.insertBefore(document.createTextNode(str +"\n"), log.firstChild);
}
function onPan(ev) {
el.className = '';
transform.translate = {
x: START_X + ev.deltaX,
y: START_Y + ev.deltaY
};
requestElementUpdate();
logEvent(ev.type);
}
var initScale = 1;
function onPinch(ev) {
if(ev.type == 'pinchstart') {
initScale = transform.scale || 1;
}
el.className = '';
transform.scale = initScale * ev.scale;
requestElementUpdate();
logEvent(ev.type);
}
var initAngle = 0;
function onRotate(ev) {
if(ev.type == 'rotatestart') {
initAngle = transform.angle || 0;
}
el.className = '';
transform.rz = 1;
transform.angle = initAngle + ev.rotation;
requestElementUpdate();
logEvent(ev.type);
}
function onSwipe(ev) {
var angle = 50;
transform.ry = (ev.direction & Hammer.DIRECTION_HORIZONTAL) ? 1 : 0;
transform.rx = (ev.direction & Hammer.DIRECTION_VERTICAL) ? 1 : 0;
transform.angle = (ev.direction & (Hammer.DIRECTION_RIGHT | Hammer.DIRECTION_UP)) ? angle : -angle;
clearTimeout(timer);
timer = setTimeout(function () {
resetElement();
}, 300);
requestElementUpdate();
logEvent(ev.type);
}
function onTap(ev) {
transform.rx = 1;
transform.angle = 25;
clearTimeout(timer);
timer = setTimeout(function () {
resetElement();
}, 200);
requestElementUpdate();
logEvent(ev.type);
}
function onDoubleTap(ev) {
transform.rx = 1;
transform.angle = 80;
clearTimeout(timer);
timer = setTimeout(function () {
resetElement();
}, 500);
requestElementUpdate();
logEvent(ev.type);
}
resetElement();
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,237 @@
/*!
* QUnit 1.14.0
* http://qunitjs.com/
*
* Copyright 2013 jQuery Foundation and other contributors
* Released under the MIT license
* http://jquery.org/license
*
* Date: 2014-01-31T16:40Z
*/
/** Font Family and Sizes */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
}
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
#qunit-tests { font-size: smaller; }
/** Resets */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
margin: 0;
padding: 0;
}
/** Header */
#qunit-header {
padding: 0.5em 0 0.5em 1em;
color: #8699A4;
background-color: #0D3349;
font-size: 1.5em;
line-height: 1em;
font-weight: 400;
border-radius: 5px 5px 0 0;
}
#qunit-header a {
text-decoration: none;
color: #C2CCD1;
}
#qunit-header a:hover,
#qunit-header a:focus {
color: #FFF;
}
#qunit-testrunner-toolbar label {
display: inline-block;
padding: 0 0.5em 0 0.1em;
}
#qunit-banner {
height: 5px;
}
#qunit-testrunner-toolbar {
padding: 0.5em 0 0.5em 2em;
color: #5E740B;
background-color: #EEE;
overflow: hidden;
}
#qunit-userAgent {
padding: 0.5em 0 0.5em 2.5em;
background-color: #2B81AF;
color: #FFF;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
#qunit-modulefilter-container {
float: right;
}
/** Tests: Pass/Fail */
#qunit-tests {
list-style-position: inside;
}
#qunit-tests li {
padding: 0.4em 0.5em 0.4em 2.5em;
border-bottom: 1px solid #FFF;
list-style-position: inside;
}
#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
display: none;
}
#qunit-tests li strong {
cursor: pointer;
}
#qunit-tests li a {
padding: 0.5em;
color: #C2CCD1;
text-decoration: none;
}
#qunit-tests li a:hover,
#qunit-tests li a:focus {
color: #000;
}
#qunit-tests li .runtime {
float: right;
font-size: smaller;
}
.qunit-assert-list {
margin-top: 0.5em;
padding: 0.5em;
background-color: #FFF;
border-radius: 5px;
}
.qunit-collapsed {
display: none;
}
#qunit-tests table {
border-collapse: collapse;
margin-top: 0.2em;
}
#qunit-tests th {
text-align: right;
vertical-align: top;
padding: 0 0.5em 0 0;
}
#qunit-tests td {
vertical-align: top;
}
#qunit-tests pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
#qunit-tests del {
background-color: #E0F2BE;
color: #374E0C;
text-decoration: none;
}
#qunit-tests ins {
background-color: #FFCACA;
color: #500;
text-decoration: none;
}
/*** Test Counts */
#qunit-tests b.counts { color: #000; }
#qunit-tests b.passed { color: #5E740B; }
#qunit-tests b.failed { color: #710909; }
#qunit-tests li li {
padding: 5px;
background-color: #FFF;
border-bottom: none;
list-style-position: inside;
}
/*** Passing Styles */
#qunit-tests li li.pass {
color: #3C510C;
background-color: #FFF;
border-left: 10px solid #C6E746;
}
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
#qunit-tests .pass .test-name { color: #366097; }
#qunit-tests .pass .test-actual,
#qunit-tests .pass .test-expected { color: #999; }
#qunit-banner.qunit-pass { background-color: #C6E746; }
/*** Failing Styles */
#qunit-tests li li.fail {
color: #710909;
background-color: #FFF;
border-left: 10px solid #EE5757;
white-space: pre;
}
#qunit-tests > li:last-child {
border-radius: 0 0 5px 5px;
}
#qunit-tests .fail { color: #000; background-color: #EE5757; }
#qunit-tests .fail .test-name,
#qunit-tests .fail .module-name { color: #000; }
#qunit-tests .fail .test-actual { color: #EE5757; }
#qunit-tests .fail .test-expected { color: #008000; }
#qunit-banner.qunit-fail { background-color: #EE5757; }
/** Result */
#qunit-testresult {
padding: 0.5em 0.5em 0.5em 2.5em;
color: #2B81AF;
background-color: #D2E0E6;
border-bottom: 1px solid #FFF;
}
#qunit-testresult .module-name {
font-weight: 700;
}
/** Fixture */
#qunit-fixture {
position: absolute;
top: -10000px;
left: -10000px;
width: 1000px;
height: 1000px;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
var utils = {
/**
* trigger simple dom event
* @param obj
* @param name
*/
triggerDomEvent: function(obj, name) {
var event = document.createEvent('Event');
event.initEvent(name, true, true);
obj.dispatchEvent(event);
},
createTouchEvent: function(name, x, y, identifier) {
var event = document.createEvent('Event');
event.initEvent('touch' + name, true, true);
event.touches = event.targetTouches = [{
clientX: x,
clientY: y,
identifier: identifier || 0
}];
//https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent.changedTouches
event.changedTouches = [{
clientX: x,
clientY: y,
identifier: identifier || 0
}];
return event;
},
dispatchTouchEvent: function(el, name, x, y, identifier) {
var event = utils.createTouchEvent(name, x, y, identifier);
el.dispatchEvent(event);
},
createHitArea: function(parent) {
if (parent == null) {
parent = document.getElementById('qunit-fixture')
}
var hitArea = document.createElement('div');
hitArea.style.background = '#eee';
hitArea.style.height = '300px';
parent.appendChild(hitArea);
return hitArea;
}
};

View File

@@ -0,0 +1,63 @@
var el,
hammer;
module('Pan Gesture', {
setup: function() {
el = document.createElement('div');
document.body.appendChild(el);
hammer = new Hammer(el, {recognizers: []});
},
teardown: function() {
document.body.removeChild(el);
hammer.destroy();
}
});
test('`panstart` and `panmove` should be recognized', function() {
expect(2);
var panMoveCount = 0;
var pan = new Hammer.Pan({threshold: 1});
hammer.add(pan);
hammer.on('panstart', function() {
ok(true);
});
hammer.on('panmove', function() {
panMoveCount++;
});
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'move', 70, 50);
utils.dispatchTouchEvent(el, 'move', 90, 50);
equal(panMoveCount, 1);
});
asyncTest('Pan event flow should be start -> left -> end', function() {
expect(1);
var pan = new Hammer.Pan({threshold: 1});
hammer.add(pan);
var eventflow = "";
var isCalledPanleft = false;
hammer.on('panstart', function() {
eventflow += "start";
});
hammer.on('panleft', function() {
if(!isCalledPanleft){
isCalledPanleft = true;
eventflow += "left";
}
});
hammer.on('panend', function() {
eventflow += "end";
isCalledPanleft = true;
});
Simulator.gestures.pan(el, { deltaX: -100, deltaY: 0 }, function() {
equal(eventflow,"startleftend");
start();
});
});

View File

@@ -0,0 +1,43 @@
var el,
hammer;
module('Pinch Gesture', {
setup: function() {
el = document.createElement('div');
document.body.appendChild(el);
hammer = new Hammer(el, {recognizers: []});
},
teardown: function() {
document.body.removeChild(el);
hammer.destroy();
}
});
asyncTest('Pinch event flow should be start -> in -> end', function() {
expect(1);
var pinch = new Hammer.Pinch({enable: true, threshold: .1});
hammer.add(pinch);
var eventflow = "";
var isFiredPinchin = false;
hammer.on('pinchstart', function() {
eventflow += "start";
});
hammer.on('pinchin', function() {
if(!isFiredPinchin){
isFiredPinchin = true;
eventflow += "in";
}
});
hammer.on('pinchend', function() {
eventflow += "end";
isFiredPinchin = false;
});
Simulator.gestures.pinch(el, { duration: 500, scale: .5 }, function() {
equal(eventflow,"startinend");
start();
});
});

View File

@@ -0,0 +1,29 @@
var el,
hammer,
swipeCount = 0;
module('Swipe Gesture', {
setup: function() {
el = utils.createHitArea();
hammer = new Hammer(el, {recognizers: []});
swipeCount = 0;
},
teardown: function() {
hammer.destroy();
}
});
test('swipe can be recognized', function() {
expect(1);
var swipe = new Hammer.Swipe({threshold: 1});
hammer.add(swipe);
hammer.on('swipe', function() {
ok(true);
start();
});
stop();
Simulator.gestures.swipe(el);
});

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Tests</title>
<link rel="stylesheet" href="assets/qunit.css">
<script src="assets/jquery.min.js"></script>
<script src="assets/lodash.compat.js"></script>
<script src="assets/qunit.js"></script>
<!--[if !IE]> --><script src="assets/blanket.js"></script><!-- <![endif]-->
<script src="assets/utils.js"></script>
<script src="../../node_modules/hammer-simulator/index.js"></script>
<script>
Simulator.setType('touch');
Simulator.events.touch.fakeSupport();
</script>
<script src="../build.js" data-cover></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="test_utils.js"></script>
<script src="test_enable.js"></script>
<script src="test_hammer.js"></script>
<script src="test_events.js"></script>
<script src="test_nested_gesture_recognizers.js"></script>
<script src="test_simultaneous_recognition.js"></script>
<script src="test_propagation_bubble.js"></script>
<script src="test_gestures.js"></script>
<script src="test_multiple_taps.js"></script>
<script src="test_require_failure.js"></script>
<script src="test_jquery_plugin.js"></script>
<script src="gestures/test_pan.js"></script>
<script src="gestures/test_pinch.js"></script>
<script src="gestures/test_swipe.js"></script>
</body>
</html>

View File

@@ -0,0 +1,171 @@
var el,
hammer,
counter;
module('Test recognizer enable', {
setup: function() {
el = utils.createHitArea();
hammer = new Hammer.Manager(el, {recognizers: []});
counter = 0;
},
teardown: function() {
hammer && hammer.destroy();
}
});
test('should disable a recognizer through the `enable` constructor parameter', function() {
expect(1);
hammer.add(new Hammer.Tap({enable: false}));
hammer.on('tap', function() {
counter++;
});
stop();
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(counter, 0);
}, 100);
});
test('should disable recognizing when the manager is disabled.', function() {
expect(1);
hammer.set({ enable: false });
hammer.add(new Hammer.Tap());
hammer.on('tap', function() {
counter++;
});
stop();
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(counter, 0);
}, 100);
});
test('should toggle a recognizer using the `set` call to the recognizer enable property', function() {
expect(2);
hammer.add(new Hammer.Tap());
hammer.on('tap', function() {
counter++;
});
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
hammer.get('tap').set({ enable: false });
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
});
test('should accept the `enable` constructor parameter as function', function() {
expect(2);
var canRecognizeTap = false;
var tap = new Hammer.Tap({
enable: function() {
return canRecognizeTap;
}
});
hammer.add(tap);
hammer.on('tap', function() {
counter++;
});
stop();
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(counter, 0);
canRecognizeTap = true;
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
}, 100);
});
test('should accept a function parameter with `set`', function() {
expect(3);
hammer.add(new Hammer.Tap());
hammer.on('tap', function() {
counter++;
});
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
var canRecognizeTap = false;
hammer.get('tap').set({ enable: function() {
return canRecognizeTap;
}});
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
canRecognizeTap = true;
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 2);
});
test('should pass the recognizer and optional the input parameter to the `enable` callback', function() {
expect(2);
var tap;
// the enable function is called initially to setup the touch-action property
// at that moment there isnt any input
var canEnable = function(recognizer, input) {
equal(recognizer, tap);
return true;
};
tap = new Hammer.Tap({enable: canEnable});
hammer.add(tap);
utils.dispatchTouchEvent(el, 'start', 50, 50);
});
test('should toggle based on other object method', function() {
expect(2);
var view = {
state: 0,
canRecognizeTap: function(recognizer, input) {
return this.state !== 0;
}
};
hammer.add(new Hammer.Tap({enable: function(rec, input) { return view.canRecognizeTap(rec, input); } }));
hammer.on('tap', function() {
counter++;
});
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 0);
view.state = 1;
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
equal(counter, 1);
});

View File

@@ -0,0 +1,61 @@
module('eventEmitter');
test('test the eventemitter', function() {
expect(6);
var ee = new Hammer.Manager(utils.createHitArea());
var inputData = {
target: document.body,
srcEvent: {
preventDefault: function() {
ok(true, 'preventDefault ref');
},
target: document.body
}
};
function event3Handler() {
ok(true, 'emitted event3');
}
ee.on('testEvent1', function() {
ok(true, 'emitted event');
});
ee.on('testEvent2', function(ev) {
ok(true, 'emitted event');
ev.preventDefault();
ok(ev.target === document.body, 'target is the body');
});
ee.on('testEvent3', event3Handler);
ee.emit('testEvent1', inputData);
ee.emit('testEvent2', inputData);
ee.emit('testEvent3', inputData);
// unbind testEvent2
ee.off('testEvent2');
ee.off('testEvent3', event3Handler);
ee.emit('testEvent1', inputData); // should trigger testEvent1 again
ee.emit('testEvent2', inputData); // doenst trigger a thing
ee.emit('testEvent3', inputData); // doenst trigger a thing
// destroy
ee.destroy();
ee.emit('testEvent1', inputData); // doenst trigger a thing
ee.emit('testEvent2', inputData); // doenst trigger a thing
ee.emit('testEvent3', inputData); // doenst trigger a thing
});
/*
* Hammer.Manager.off method : exception handling
*/
test("When Hammer.Manager didn't attach an event, 'off' method is ignored", function() {
var count = 0;
hammer = new Hammer(el, { inputTarget: document.body });
hammer.off("swipeleft", function(e) {
count++;
});
ok(true, "nothing");
});

View File

@@ -0,0 +1,208 @@
// TODO: this tests fails because tapRecognizer changes
// it could be that tapRecognizer setup its BEGAN state and
// disable the other gesture recognition
var el, hammer, events;
var allGestureEvents = [
'tap doubletap press',
'pinch pinchin pinchout pinchstart pinchmove pinchend pinchcancel',
'rotate rotatestart rotatemove rotateend rotatecancel',
'pan panstart panmove panup pandown panleft panright panend pancancel',
'swipe swipeleft swiperight swipeup swipedown'
].join(' ');
module('Gesture recognition', {
setup: function() {
el = utils.createHitArea();
hammer = new Hammer(el);
hammer.get('pinch')
.set({ // some threshold, since the simulator doesnt stays at scale:1 when rotating
enable: true,
threshold: .1
});
hammer.get('rotate')
.set({ enable: true });
hammer.on(allGestureEvents, function(ev) {
events[ev.type] = true;
});
events = {};
},
teardown: function() {
hammer && hammer.destroy();
events = null;
}
});
asyncTest('recognize pan', function() {
expect(1);
Simulator.gestures.pan(el, { duration: 500, deltaX: 100, deltaY: 0 }, function() {
start();
deepEqual(events, {
pan: true,
panstart: true,
panmove: true,
panright: true,
panend: true
});
});
});
asyncTest('recognize press', function() {
expect(1);
Simulator.gestures.press(el, null, function() {
start();
deepEqual(events, {
press: true
});
});
});
asyncTest('recognize swipe', function() {
expect(1);
Simulator.gestures.swipe(el, { duration: 300, deltaX: 400, deltaY: 0 }, function() {
start();
deepEqual(events, {
pan: true,
panstart: true,
panmove: true,
panright: true,
panend: true,
swipe: true,
swiperight: true
});
});
});
asyncTest('recognize pinch', function() {
expect(1);
Simulator.gestures.pinch(el, { duration: 500, scale: .5 }, function() {
start();
deepEqual(events, {
pinch: true,
pinchstart: true,
pinchmove: true,
pinchend: true,
pinchin: true
});
});
});
asyncTest('recognize children multitouch pinch', function() {
expect(1);
var el1 = utils.createHitArea(el),
el2 = utils.createHitArea(el);
Simulator.gestures.pinch([el1, el2], { duration: 500, scale: .5 }, function() {
start();
deepEqual(events, {
pinch: true,
pinchstart: true,
pinchmove: true,
pinchend: true,
pinchin: true
});
});
});
asyncTest('recognize parent-child multitouch pinch', function() {
expect(1);
var el1 = utils.createHitArea(el);
Simulator.gestures.pinch([el, el1], { duration: 100, scale: .5 }, function() {
start();
deepEqual(events, {
pinch: true,
pinchstart: true,
pinchmove: true,
pinchend: true,
pinchin: true
});
});
});
asyncTest('recognize rotate', function() {
expect(1);
Simulator.gestures.rotate(el, { duration: 500, scale: 1 }, function() {
start();
deepEqual(events, {
rotate: true,
rotatestart: true,
rotatemove: true,
rotateend: true
});
});
});
asyncTest('recognize multitouch rotate', function() {
expect(1);
var el1 = utils.createHitArea(el);
Simulator.gestures.rotate([el, el1], { duration: 500, scale: 1 }, function() {
start();
deepEqual(events, {
rotate: true,
rotatestart: true,
rotatemove: true,
rotateend: true
});
});
});
asyncTest('recognize rotate and pinch simultaneous', function() {
expect(1);
Simulator.gestures.pinchRotate(el, { duration: 500, scale: 2 }, function() {
start();
deepEqual(events, {
rotate: true,
rotatestart: true,
rotatemove: true,
rotateend: true,
pinch: true,
pinchstart: true,
pinchmove: true,
pinchend: true,
pinchout: true
});
});
});
asyncTest('don\'t recognize pan and swipe when moving down, when only horizontal is allowed', function() {
expect(1);
Simulator.gestures.swipe(el, { duration: 250, deltaX: 0, deltaZ: 200 }, function() {
start();
deepEqual(events, { });
});
});
asyncTest('don\'t recognize press if duration is too short.', function() {
expect(1);
Simulator.gestures.press(el, { duration: 240 });
setTimeout(function() {
start();
deepEqual(events, { tap: true }, 'Tap gesture has been recognized.');
}, 275);
});
asyncTest('don\'t recognize tap if duration is too long.', function() {
expect(1);
Simulator.gestures.tap(el, { duration: 255 });
setTimeout(function() {
start();
deepEqual(events, { press: true }, 'Press gesture has been recognized.');
}, 275);
});

View File

@@ -0,0 +1,187 @@
var el, el2,
hammer, hammer2;
module('Tests', {
setup: function() {
el = utils.createHitArea();
el2 = utils.createHitArea();
},
teardown: function() {
if (hammer) {
hammer.destroy();
hammer = null;
}
if (hammer2) {
hammer2.destroy();
hammer2 = null;
}
}
});
test('hammer shortcut', function() {
expect(2);
Hammer.defaults.touchAction = 'pan-y';
hammer = Hammer(el);
ok(hammer instanceof Hammer.Manager, 'returns an instance of Manager');
ok(hammer.touchAction.actions == Hammer.defaults.touchAction, 'set the default touchAction');
});
test('hammer shortcut with options', function() {
expect(2);
hammer = Hammer(el, {
touchAction: 'none'
});
ok(hammer instanceof Hammer.Manager, 'returns an instance of Manager');
ok(hammer.touchAction.actions == 'none', 'set the default touchAction');
});
/* Creating a hammer instance does not work on the same way
* when using Hammer or Hammer.Manager.
*
* This can confuse developers who read tests to use the library when doc is missing.
*/
test('Hammer and Hammer.Manager constructors work exactly on the same way.', function() {
expect(2);
hammer = new Hammer(el, {});
equal(Hammer.defaults.preset.length, hammer.recognizers.length);
hammer2 = new Hammer.Manager(el, {});
equal(0, hammer2.recognizers.length);
});
/* DOC to disable default recognizers should be added.
*
* - Hammer(el). IMO: Currently, well done.
* - Hammer(el, {}) . IMO: should disable default recognizers
* - Hammer(el, {recognizers: null}). IMO: now, it fails.
* - Hammer(el, {recognizers: []}). It works, but it is likely not intuitive.
*/
test('A Hammer instance can be setup to not having default recognizers.', function() {
expect(1);
hammer = new Hammer(el, { recognizers: false });
equal(0, hammer.recognizers.length);
});
/* The case was when I added a custom tap event which was added to the default
* recognizers, and my custom tap gesture wasn't working (I do not know exactly the reason),
* but removing the default recognizers solved the issue.
*/
test('Adding the same recognizer type should remove the old recognizer', function() {
expect(4);
hammer = new Hammer(el);
ok(!!hammer.get('tap'));
equal(7, hammer.recognizers.length);
var newTap = new Hammer.Tap({time: 1337});
hammer.add(newTap);
equal(7, hammer.recognizers.length);
equal(1337, hammer.get('tap').options.time);
});
/*
* Swipe gesture:
* - in this tests, it does not update input.velocity ( always 0)
* - does not fire swipeleft or swiperight events
*/
asyncTest('Swiping to the left should fire swipeleft event', function() {
expect(2);
hammer = new Hammer(el, {recognizers: []});
hammer.add(new Hammer.Swipe());
hammer.on('swipe swipeleft', function() {
ok(true);
});
Simulator.gestures.swipe(el, {pos: [300, 300], deltaY: 0, deltaX: -200}, function() {
start();
});
});
/*
* Input target change
*/
asyncTest('Should detect input while on other element', function() {
expect(1);
hammer = new Hammer(el, { inputTarget: document.body });
hammer.on('tap', function() {
ok(true);
});
Simulator.gestures.tap(document.body, null, function() {
start();
});
});
/* Hammer.Manager constructor accepts a "recognizers" option in which each
* element is an array representation of a Recognizer.
*/
test('Hammer.Manager accepts recognizers as arrays.', function() {
expect(4);
hammer = new Hammer.Manager(el, {
recognizers: [
[Hammer.Swipe],
[Hammer.Pinch],
[Hammer.Rotate],
[Hammer.Pan, { direction: Hammer.DIRECTION_UP }, ['swipe', 'pinch'], ['rotate']]
]
});
equal(4, hammer.recognizers.length);
var recognizerActual = hammer.recognizers[3];
equal(recognizerActual.options.direction, Hammer.DIRECTION_UP);
equal(2, Object.keys(recognizerActual.simultaneous).length);
equal(1, recognizerActual.requireFail.length);
});
/*
* Removing a recognizer which cannot be found would errantly remove the last recognizer in the
* manager's list.
*/
test('Remove non-existent recognizer.', function() {
expect(1);
hammer = new Hammer(el, {recognizers: []});
hammer.add(new Hammer.Swipe());
hammer.remove('tap');
equal(1, hammer.recognizers.length);
});
test('check whether Hammer.defaults.cssProps is restored', function() {
var beforeCssProps = {
userSelect: 'text',
touchSelect: 'grippers',
touchCallout: 'default',
contentZooming: 'chained',
userDrag: 'element',
tapHighlightColor: 'rgba(0, 1, 0, 0)'
};
var prop;
Hammer.each(Hammer.defaults.cssProps, function(value, name) {
prop = Hammer.prefixed(el.style, name);
if (prop) {
el.style[prop] = beforeCssProps[name];
}
});
hammer = Hammer(el);
hammer.destroy();
hammer = null;
Hammer.each(Hammer.defaults.cssProps, function(value, name) {
prop = Hammer.prefixed(el.style, name);
if (prop) {
equal(el.style[prop], beforeCssProps[name], "check if " + name + " is restored");
}
});
});

View File

@@ -0,0 +1,59 @@
var el, hammer, events;
var jQueryPluginPath = '../../node_modules/jquery-hammerjs/jquery.hammer.js';
module('jQuery plugin', {
setup: function() {
el = utils.createHitArea();
events = {};
},
teardown: function() {
hammer && hammer.destroy();
}
});
asyncTest('trigger pan with jQuery', function() {
expect(2);
$.getScript(jQueryPluginPath, function() {
jQuery(el).hammer();
jQuery(el).bind('panstart pan panmove panright panend', function(ev) {
if (ev.gesture) {
events[ev.type] = true;
}
});
Simulator.gestures.pan(el, { deltaX: 50, deltaY: 0 }, function() {
start();
deepEqual(events, {
pan: true,
panstart: true,
panmove: true,
panright: true,
panend: true
});
ok(jQuery(el).data('hammer') instanceof Hammer.Manager, 'data attribute refers to the instance');
});
});
});
asyncTest('trigger pan without jQuery should still work', function() {
expect(1);
var hammer = Hammer(el);
hammer.on('panstart pan panmove panright panend', function(ev) {
events[ev.type] = true;
});
Simulator.gestures.pan(el, { deltaX: 50, deltaY: 0 }, function() {
start();
deepEqual(events, {
pan: true,
panstart: true,
panmove: true,
panright: true,
panend: true
});
});
});

View File

@@ -0,0 +1,90 @@
var el, hammer;
var tripleTapCount = 0,
doubleTapCount = 0,
tapCount = 0;
module('Tap delay', {
setup: function() {
el = utils.createHitArea();
hammer = new Hammer(el, {recognizers: []});
var tap = new Hammer.Tap();
var doubleTap = new Hammer.Tap({event: 'doubleTap', taps: 2 });
var tripleTap = new Hammer.Tap({event: 'tripleTap', taps: 3 });
hammer.add([tripleTap, doubleTap, tap]);
tripleTap.recognizeWith([doubleTap, tap]);
doubleTap.recognizeWith(tap);
doubleTap.requireFailure(tripleTap);
tap.requireFailure([tripleTap, doubleTap]);
tripleTapCount = 0;
doubleTapCount = 0;
tapCount = 0;
hammer.on('tap', function() {
tapCount++;
});
hammer.on('doubleTap', function() {
doubleTapCount++;
});
hammer.on('tripleTap', function() {
tripleTapCount++;
});
},
teardown: function() {
hammer.destroy();
}
});
asyncTest('When a tripleTap is fired, doubleTap and Tap should not be recognized', function() {
expect(3);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(tripleTapCount, 1, 'one tripletap event');
equal(doubleTapCount, 0, 'no doubletap event');
equal(tapCount, 0, 'no singletap event');
}, 350);
});
asyncTest('When a doubleTap is fired, tripleTap and Tap should not be recognized', function() {
expect(3);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(tripleTapCount, 0);
equal(doubleTapCount, 1);
equal(tapCount, 0);
}, 350);
});
asyncTest('When a tap is fired, tripleTap and doubleTap should not be recognized', function() {
expect(3);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'end', 50, 50);
setTimeout(function() {
start();
equal(tripleTapCount, 0);
equal(doubleTapCount, 0);
equal(tapCount, 1);
}, 350);
});

View File

@@ -0,0 +1,167 @@
var parent,
child,
hammerChild,
hammerParent;
module('Nested gesture recognizers (Tap Child + Pan Parent)', {
setup: function() {
parent = document.createElement('div');
child = document.createElement('div');
document.getElementById('qunit-fixture').appendChild(parent);
parent.appendChild(child);
hammerParent = new Hammer.Manager(parent, {
touchAction: 'none'
});
hammerChild = new Hammer.Manager(child, {
touchAction: 'none'
});
hammerChild.add(new Hammer.Tap());
hammerParent.add(new Hammer.Pan({threshold: 5, pointers: 1}));
},
teardown: function() {
hammerChild.destroy();
hammerParent.destroy();
}
});
test('Tap on the child', function() {
expect(1);
hammerChild.on('tap', function() {
ok(true);
});
hammerParent.on('tap', function() {
throw new Error('tap should not fire on parent');
});
utils.dispatchTouchEvent(child, 'start', 0, 10);
utils.dispatchTouchEvent(child, 'end', 0, 10);
});
test('Panning on the child should fire parent pan and should not fire child tap event', function() {
expect(1);
hammerChild.on('tap', function() {
throw new Error('tap should not fire on parent');
});
hammerParent.on('panend', function() {
ok(true);
});
utils.dispatchTouchEvent(child, 'start', 10, 0);
utils.dispatchTouchEvent(child, 'move', 20, 0);
utils.dispatchTouchEvent(child, 'end', 30, 0);
});
/*
// test (optional pointers validation)
test('Panning with one finger down on child, other on parent', function () {
expect(1);
var event,
touches;
hammerParent.on('panend', function () {
ok(true);
});
// one finger one child
utils.dispatchTouchEvent(child, 'start', 10, 0, 0);
utils.dispatchTouchEvent(parent, 'start', 12, 0, 1);
touches = [
{clientX: 20, clientY: 0, identifier: 0 },
{clientX: 20, clientY: 0, identifier: 1 }
];
event = document.createEvent('Event');
event.initEvent('touchmove', true, true);
event.touches = touches;
event.changedTouches = touches;
parent.dispatchEvent(event);
touches = [
{clientX: 30, clientY: 0, identifier: 0 },
{clientX: 30, clientY: 0, identifier: 1 }
];
event = document.createEvent('Event');
event.initEvent('touchend', true, true);
event.touches = touches;
event.changedTouches = touches;
parent.dispatchEvent(event);
});
*/
var pressPeriod = 600;
module('Nested gesture recognizers (Press Child + Pan Parent)', {
setup: function() {
parent = document.createElement('div');
child = document.createElement('div');
document.getElementById('qunit-fixture').appendChild(parent);
parent.appendChild(child);
hammerParent = new Hammer.Manager(parent, {
touchAction: 'none'
});
hammerChild = new Hammer.Manager(child, {
touchAction: 'none'
});
hammerChild.add(new Hammer.Press({time: pressPeriod}));
hammerParent.add(new Hammer.Pan({threshold: 5, pointers: 1}));
},
teardown: function() {
hammerChild.destroy();
hammerParent.destroy();
}
});
test('Press on the child', function() {
expect(1);
hammerChild.on('press', function() {
ok(true);
});
hammerParent.on('press', function() {
throw new Error('press should not fire on parent');
});
utils.dispatchTouchEvent(child, 'start', 0, 10);
stop();
setTimeout(function() {
start();
}, pressPeriod);
});
test('When Press is followed by Pan on the same element, both gestures are recognized', function() {
expect(2);
hammerChild.on('press', function() {
ok(true);
});
hammerParent.on('panend', function() {
ok(true);
});
utils.dispatchTouchEvent(child, 'start', 0, 10);
stop();
setTimeout(function() {
start();
utils.dispatchTouchEvent(child, 'move', 10, 10);
utils.dispatchTouchEvent(child, 'move', 20, 10);
utils.dispatchTouchEvent(child, 'move', 30, 10);
utils.dispatchTouchEvent(child, 'end', 30, 10);
}, pressPeriod);
});

View File

@@ -0,0 +1,56 @@
var parent,
child,
hammerChild,
hammerParent;
module('Propagation (Tap in Child and Parent)', {
setup: function() {
parent = document.createElement('div');
child = document.createElement('div');
document.getElementById('qunit-fixture').appendChild(parent);
parent.appendChild(child);
hammerParent = new Hammer.Manager(parent);
hammerChild = new Hammer.Manager(child);
hammerChild.add(new Hammer.Tap());
hammerParent.add(new Hammer.Tap());
},
teardown: function() {
hammerChild.destroy();
hammerParent.destroy();
}
});
test('Tap on the child, fires also the tap event to the parent', function() {
expect(2);
hammerChild.on('tap', function() {
ok(true);
});
hammerParent.on('tap', function() {
ok(true);
});
utils.dispatchTouchEvent(child, 'start', 0, 10);
utils.dispatchTouchEvent(child, 'end', 0, 10);
});
test('When tap on the child and the child stops the input event propagation, the tap event does not get fired in the parent', function() {
expect(1);
hammerChild.on('tap', function() {
ok(true);
});
hammerParent.on('tap', function() {
throw new Error('parent tap gesture should not be recognized');
});
child.addEventListener('touchend', function(ev) {
ev.stopPropagation();
});
utils.dispatchTouchEvent(child, 'start', 0, 10);
utils.dispatchTouchEvent(child, 'end', 0, 10);
});

View File

@@ -0,0 +1,111 @@
var el,
hammer,
pressPeriod = 200,
pressThreshold = 20,
pressCount = 0,
panStartCount = 0,
swipeCount = 0;
module('Require Failure ( Swipe & Press )', {
setup: function() {
el = utils.createHitArea();
hammer = new Hammer(el, {recognizers: []});
var swipe = new Hammer.Swipe({threshold: 1});
var press = new Hammer.Press({time: pressPeriod, threshold: pressThreshold});
hammer.add(swipe);
hammer.add(press);
swipe.recognizeWith(press);
press.requireFailure(swipe);
pressCount = 0;
swipeCount = 0;
hammer.on('press', function() {
pressCount++;
});
hammer.on('swipe', function() {
swipeCount++;
});
},
teardown: function() {
hammer.destroy();
}
});
asyncTest('When swipe does not recognize the gesture, a press gesture can be fired', function() {
expect(1);
utils.dispatchTouchEvent(el, 'start', 50, 50);
setTimeout(function() {
start();
equal(pressCount, 1);
}, pressPeriod + 100);
});
asyncTest('When swipe does recognize the gesture, a press gesture cannot be fired', function() {
expect(2);
Simulator.gestures.swipe(el, null, function() {
start();
ok(swipeCount > 0, 'swipe gesture should be recognizing');
equal(pressCount, 0, 'press gesture should not be recognized because swipe gesture is recognizing');
});
});
module('Require Failure ( Pan & Press )', {
setup: function() {
el = document.createElement('div');
document.body.appendChild(el);
hammer = new Hammer(el, {recognizers: []});
var pan = new Hammer.Pan({threshold: 1});
var press = new Hammer.Press({time: pressPeriod, threshold: pressThreshold});
hammer.add([pan, press]);
pan.recognizeWith(press);
press.requireFailure(pan);
pressCount = 0;
panStartCount = 0;
hammer.on('press', function() {
pressCount++;
});
hammer.on('panstart', function() {
panStartCount++;
});
},
teardown: function() {
document.body.removeChild(el);
hammer.destroy();
}
});
asyncTest('When pan does not recognize the gesture, a press gesture can be fired', function() {
expect(1);
utils.dispatchTouchEvent(el, 'start', 50, 50);
setTimeout(function() {
start();
equal(pressCount, 1);
}, pressPeriod + 100);
});
asyncTest('When pan recognizes the gesture, a press gesture cannot be fired', function() {
expect(2);
utils.dispatchTouchEvent(el, 'start', 50, 50);
utils.dispatchTouchEvent(el, 'move', 50 + pressThreshold / 4, 50);
setTimeout(function() {
start();
ok(panStartCount > 0, 'pan gesture should be recognizing');
equal(pressCount, 0, 'press gesture should not be recognized because pan gesture is recognizing');
}, pressPeriod + 100);
});

View File

@@ -0,0 +1,234 @@
var el,
hammer;
module('Simultaenous recognition', {
setup: function() {
el = utils.createHitArea()
},
teardown: function() {
hammer && hammer.destroy();
}
});
asyncTest('should pinch and pan simultaneously be recognized when enabled', function() {
expect(4);
var panCount = 0,
pinchCount = 0;
hammer = new Hammer.Manager(el, {
touchAction: 'none'
});
hammer.add(new Hammer.Pan({threshold: 5, pointers: 2}));
var pinch = new Hammer.Pinch({ threshold: 0, pointers: 2});
hammer.add(pinch);
pinch.recognizeWith(hammer.get('pan'));
hammer.on('panend', function() {
panCount++;
});
hammer.on('pinchend', function() {
pinchCount++;
});
var executeGesture = function(cb) {
var event, touches;
touches = [
{clientX: 0, clientY: 10, identifier: 0, target: el },
{clientX: 10, clientY: 10, identifier: 1, target: el }
];
event = document.createEvent('Event');
event.initEvent('touchstart', true, true);
event.touches = touches;
event.targetTouches = touches;
event.changedTouches = touches;
el.dispatchEvent(event);
setTimeout(function() {
touches = [
{clientX: 10, clientY: 20, identifier: 0, target: el },
{clientX: 20, clientY: 20, identifier: 1, target: el }
];
event = document.createEvent('Event');
event.initEvent('touchmove', true, true);
event.touches = touches;
event.targetTouches = touches;
event.changedTouches = touches;
el.dispatchEvent(event);
}, 100);
setTimeout(function() {
touches = [
{clientX: 20, clientY: 30, identifier: 0, target: el },
{clientX: 40, clientY: 30, identifier: 1, target: el }
];
event = document.createEvent('Event');
event.initEvent('touchmove', true, true);
event.touches = touches;
event.targetTouches = touches;
event.changedTouches = touches;
el.dispatchEvent(event);
event = document.createEvent('Event');
event.initEvent('touchend', true, true);
event.touches = touches;
event.targetTouches = touches;
event.changedTouches = touches;
el.dispatchEvent(event);
cb();
}, 200);
};
// 2 gesture will be recognized
executeGesture(function() {
equal(panCount, 1);
equal(pinchCount, 1);
pinch.dropRecognizeWith(hammer.get('pan'));
// only the pan gesture will be recognized
executeGesture(function() {
equal(panCount, 2);
equal(pinchCount, 1);
start();
});
});
});
test('the first gesture should block the following gestures (Tap & DoubleTap)', function() {
expect(4);
var tapCount = 0,
doubleTapCount = 0;
hammer = new Hammer.Manager(el, {
touchAction: 'none'
});
var tap = new Hammer.Tap();
var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2});
hammer.add(tap);
hammer.add(doubleTap);
hammer.on('tap', function() {
tapCount++;
});
hammer.on('doubletap', function() {
doubleTapCount++;
});
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(tapCount, 2, 'on a double tap gesture, the tap gesture is recognized twice');
equal(doubleTapCount, 0, 'double tap gesture is not recognized because the prior tap gesture does not recognize it simultaneously');
doubleTap.recognizeWith(hammer.get('tap'));
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(tapCount, 4);
equal(doubleTapCount, 1, 'when the tap gesture is configured to work simultaneously, tap & doubleTap can be recognized simultaneously');
});
test('when disabled, the first gesture should not block gestures (Tap & DoubleTap )', function() {
expect(4);
var tapCount = 0,
doubleTapCount = 0;
hammer = new Hammer.Manager(el, {
touchAction: 'none'
});
var tap = new Hammer.Tap();
var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2});
hammer.add(tap);
hammer.add(doubleTap);
hammer.on('tap', function() {
tapCount++;
});
hammer.on('doubletap', function() {
doubleTapCount++;
});
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(tapCount, 2, 'on a double tap gesture, the tap gesture is recognized twice');
equal(doubleTapCount, 0, 'double tap gesture is not recognized because the prior tap gesture does not recognize it simultaneously');
hammer.get('tap').set({ enable: false });
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(tapCount, 2, 'tap gesture should not be recognized when the recognizer is disabled');
equal(doubleTapCount, 1, 'when the tap gesture is disabled, doubleTap can be recognized');
});
test('the first gesture should block the following gestures (DoubleTap & Tap)', function() {
expect(4);
var tapCount = 0,
doubleTapCount = 0;
hammer = new Hammer.Manager(el, {
touchAction: 'none'
});
var tap = new Hammer.Tap();
var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2});
hammer.add(doubleTap);
hammer.add(tap);
hammer.on('tap', function() {
tapCount++;
});
hammer.on('doubletap', function() {
doubleTapCount++;
});
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(doubleTapCount, 1, 'double tap is recognized');
equal(tapCount, 1, 'tap is detected, the doubletap is only catched by the doubletap recognizer');
// doubletap and tap together
doubleTap.recognizeWith(hammer.get('tap'));
doubleTapCount = 0;
tapCount = 0;
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
utils.dispatchTouchEvent(el, 'start', 0, 10);
utils.dispatchTouchEvent(el, 'end', 0, 10);
equal(doubleTapCount, 1);
equal(tapCount, 2, 'when the tap gesture is configured to work simultaneously, tap & doubleTap can be recognized simultaneously');
});

View File

@@ -0,0 +1,164 @@
module('utils');
// for the tests, all hammer properties and methods of Hammer are exposed to window.$H
test('get/set prefixed util', function() {
ok(_.isUndefined($H.prefixed(window, 'FakeProperty')), 'non existent property returns undefined');
window.webkitFakeProperty = 1337;
ok($H.prefixed(window, 'FakeProperty') == 'webkitFakeProperty', 'existent prefixed property returns the prefixed name');
delete window.webkitFakeProperty;
});
test('fnBind', function() {
var context = { a: true };
$H.bindFn(function(b) {
ok(this.a === true, 'bindFn scope');
ok(b === 123, 'bindFn argument');
}, context)(123);
});
test('Inherit objects', function() {
function Base() {
this.name = true;
}
function Child() {
Base.call(this);
}
$H.inherit(Child, Base, {
newMethod: function() {
}
});
var inst = new Child();
ok(inst.name == true, 'child has extended from base');
ok(inst.newMethod, 'child has a new method');
ok(Child.prototype.newMethod, 'child has a new prototype method');
ok(inst instanceof Child, 'is instanceof Child');
ok(inst instanceof Base, 'is instanceof Base');
ok(inst._super === Base.prototype, '_super is ref to prototype of Base');
});
test('toArray', function() {
ok(_.isArray($H.toArray({ 0: true, 1: 'second', length: 2 })), 'converted an array-like object to an array');
ok(_.isArray($H.toArray([true, true])), 'array stays an array');
});
test('inArray', function() {
ok($H.inArray([1, 2, 3, 4, 'hammer'], 'hammer') === 4, 'found item and returned the index');
ok($H.inArray([1, 2, 3, 4, 'hammer'], 'notfound') === -1, 'not found an item and returned -1');
ok($H.inArray([
{id: 2},
{id: 24}
], '24', 'id') === 1, 'find by key and return the index');
ok($H.inArray([
{id: 2},
{id: 24}
], '22', 'id') === -1, 'not found by key and return -1');
});
test('splitStr', function() {
deepEqual($H.splitStr(' a b c d '), ['a', 'b', 'c', 'd'], 'str split valid');
});
test('uniqueArray', function() {
deepEqual($H.uniqueArray([
{id: 1},
{id: 2},
{id: 2}
], 'id'), [
{id: 1},
{id: 2}
], 'remove duplicate ids')
});
test('boolOrFn', function() {
equal($H.boolOrFn(true), true, 'Passing an boolean');
equal($H.boolOrFn(false), false, 'Passing an boolean');
equal($H.boolOrFn(function() {
return true;
}), true, 'Passing an boolean');
equal($H.boolOrFn(1), true, 'Passing an integer');
});
test('hasParent', function() {
var parent = document.createElement('div'),
child = document.createElement('div');
document.body.appendChild(parent);
parent.appendChild(child);
equal($H.hasParent(child, parent), true, 'Found parent');
equal($H.hasParent(parent, child), false, 'Not in parent');
document.body.removeChild(parent);
});
test('each', function() {
var object = { hi: true };
var array = ['a', 'b', 'c'];
var loop;
loop = false;
$H.each(object, function(value, key) {
if (key == 'hi' && value === true) {
loop = true;
}
});
ok(loop, 'object loop');
loop = 0;
$H.each(array, function(value, key) {
if (value) {
loop++;
}
});
ok(loop == 3, 'array loop');
loop = 0;
array.forEach = null;
$H.each(array, function(value, key) {
if (value) {
loop++;
}
});
ok(loop == 3, 'array loop without Array.forEach');
});
test('assign', function() {
expect(2);
deepEqual(
$H.assign(
{a: 1, b: 3},
{b: 2, c: 3}
),
{a: 1, b: 2, c: 3},
'Simple extend'
);
var src = { foo: true };
var dest = $H.assign({}, src);
src.foo = false;
deepEqual(dest, {foo: true}, 'Clone reference');
});
test('test add/removeEventListener', function() {
function handleEvent() {
ok(true, 'triggered event');
}
expect(2);
$H.addEventListeners(window, 'testEvent1 testEvent2 ', handleEvent);
utils.triggerDomEvent(window, 'testEvent1');
utils.triggerDomEvent(window, 'testEvent2');
$H.removeEventListeners(window, ' testEvent1 testEvent2 ', handleEvent);
utils.triggerDomEvent(window, 'testEvent1');
utils.triggerDomEvent(window, 'testEvent2');
});