formstone
Version:
Library of modular front end components.
522 lines (432 loc) • 14.1 kB
JavaScript
/* global define */
(function(factory) {
if (typeof define === "function" && define.amd) {
define([
"jquery",
"./core"
], factory);
} else {
factory(jQuery, Formstone);
}
}(function($, Formstone) {
"use strict";
/**
* @method private
* @name construct
* @description Builds instance.
* @param data [object] "Instance data"
*/
function construct(data) {
data.touches = [];
data.touching = false;
this.on(Events.dragStart, Functions.killEvent);
if (data.swipe) {
data.pan = true;
}
if (data.scale) {
data.axis = false;
}
data.axisX = data.axis === "x";
data.axisY = data.axis === "y";
if (Formstone.support.pointer) {
var action = "";
if (!data.axis || (data.axisX && data.axisY)) {
action = "none";
} else {
if (data.axisX) {
action += " pan-y";
}
if (data.axisY) {
action += " pan-x";
}
}
touchAction(this, action);
this.on(Events.pointerDown, data, onTouch);
} else {
this.on(Events.touchStart, data, onTouch)
.on(Events.mouseDown, data, onPointerStart);
}
}
/**
* @method private
* @name destruct
* @description Tears down instance.
* @param data [object] "Instance data"
*/
function destruct(data) {
this.off(Events.namespace);
touchAction(this, "");
}
/**
* @method private
* @name onTouch
* @description Delegates touch events.
* @param e [object] "Event data"
*/
function onTouch(e) {
// Stop panning and zooming
if (e.preventManipulation) {
e.preventManipulation();
}
var data = e.data,
oe = e.originalEvent;
if (oe.type.match(/(up|end|cancel)$/i)) {
onPointerEnd(e);
return;
}
if (oe.pointerId) {
// Normalize MS pointer events back to standard touches
var activeTouch = false;
for (var i in data.touches) {
if (data.touches[i].id === oe.pointerId) {
activeTouch = true;
data.touches[i].pageX = oe.pageX;
data.touches[i].pageY = oe.pageY;
}
}
if (!activeTouch) {
data.touches.push({
id: oe.pointerId,
pageX: oe.pageX,
pageY: oe.pageY
});
}
} else {
// Alias normal touches
data.touches = oe.touches;
}
// Delegate touch actions
if (oe.type.match(/(down|start)$/i)) {
onPointerStart(e);
} else if (oe.type.match(/move$/i)) {
onPointerMove(e);
}
}
/**
* @method private
* @name onPointerStart
* @description Handles pointer start.
* @param e [object] "Event data"
*/
function onPointerStart(e) {
var data = e.data,
touch = ($.type(data.touches) !== "undefined" && data.touches.length) ? data.touches[0] : null;
if (touch) {
data.$el.off(Events.mouseDown);
}
if (!data.touching) {
data.startE = e.originalEvent;
data.startX = (touch) ? touch.pageX : e.pageX;
data.startY = (touch) ? touch.pageY : e.pageY;
data.startT = new Date().getTime();
data.scaleD = 1;
data.passed = false;
}
// Clear old click events
if (data.$links) {
data.$links.off(Events.click);
}
// Pan / Scale
var newE = buildEvent(data.scale ? Events.scaleStart : Events.panStart, e, data.startX, data.startY, data.scaleD, 0, 0, "", "");
if (data.scale && data.touches && data.touches.length >= 2) {
var t = data.touches;
data.pinch = {
startX: midpoint(t[0].pageX, t[1].pageX),
startY: midpoint(t[0].pageY, t[1].pageY),
startD: pythagorus((t[1].pageX - t[0].pageX), (t[1].pageY - t[0].pageY))
};
newE.pageX = data.startX = data.pinch.startX;
newE.pageY = data.startY = data.pinch.startY;
}
// Only bind at first touch
if (!data.touching) {
data.touching = true;
if (data.pan && !touch) {
$Window.on(Events.mouseMove, data, onPointerMove)
.on(Events.mouseUp, data, onPointerEnd);
}
if (Formstone.support.pointer) {
$Window.on([
Events.pointerMove,
Events.pointerUp,
Events.pointerCancel
].join(" "), data, onTouch);
} else {
$Window.on([
Events.touchMove,
Events.touchEnd,
Events.touchCancel
].join(" "), data, onTouch);
}
data.$el.trigger(newE);
}
}
/**
* @method private
* @name onPointerMove
* @description Handles pointer move.
* @param e [object] "Event data"
*/
function onPointerMove(e) {
var data = e.data,
touch = ($.type(data.touches) !== "undefined" && data.touches.length) ? data.touches[0] : null,
newX = (touch) ? touch.pageX : e.pageX,
newY = (touch) ? touch.pageY : e.pageY,
deltaX = newX - data.startX,
deltaY = newY - data.startY,
dirX = (deltaX > 0) ? "right" : "left",
dirY = (deltaY > 0) ? "down" : "up",
movedX = Math.abs(deltaX) > TouchThreshold,
movedY = Math.abs(deltaY) > TouchThreshold;
if (!data.passed && data.axis && ((data.axisX && movedY) || (data.axisY && movedX))) {
// if axis and moved in opposite direction
onPointerEnd(e);
} else {
if (!data.passed && (!data.axis || (data.axis && (data.axisX && movedX) || (data.axisY && movedY)))) {
// if has axis and moved in same direction
data.passed = true;
}
if (data.passed) {
Functions.killEvent(e);
Functions.killEvent(data.startE);
}
// Pan / Scale
var fire = true,
newE = buildEvent(data.scale ? Events.scale : Events.pan, e, newX, newY, data.scaleD, deltaX, deltaY, dirX, dirY);
if (data.scale) {
if (data.touches && data.touches.length >= 2) {
var t = data.touches;
data.pinch.endX = midpoint(t[0].pageX, t[1].pageX);
data.pinch.endY = midpoint(t[0].pageY, t[1].pageY);
data.pinch.endD = pythagorus((t[1].pageX - t[0].pageX), (t[1].pageY - t[0].pageY));
data.scaleD = (data.pinch.endD / data.pinch.startD);
newE.pageX = data.pinch.endX;
newE.pageY = data.pinch.endY;
newE.scale = data.scaleD;
newE.deltaX = data.pinch.endX - data.pinch.startX;
newE.deltaY = data.pinch.endY - data.pinch.startY;
} else if (!data.pan) {
fire = false;
}
}
if (fire) {
data.$el.trigger(newE);
}
}
}
/**
* @method private
* @name onPointerEnd
* @description Handles pointer end / cancel.
* @param e [object] "Event data"
*/
function onPointerEnd(e) {
var data = e.data;
// Pan / Swipe / Scale
var touch = ($.type(data.touches) !== "undefined" && data.touches.length) ? data.touches[0] : null,
newX = (touch) ? touch.pageX : e.pageX,
newY = (touch) ? touch.pageY : e.pageY,
deltaX = newX - data.startX,
deltaY = newY - data.startY,
endT = new Date().getTime(),
eType = data.scale ? Events.scaleEnd : Events.panEnd,
dirX = (deltaX > 0) ? "right" : "left",
dirY = (deltaY > 0) ? "down" : "up",
movedX = Math.abs(deltaX) > 1,
movedY = Math.abs(deltaY) > 1;
// Swipe
if (data.swipe && Math.abs(deltaX) > TouchThreshold && (endT - data.startT) < TouchTime) {
eType = Events.swipe;
}
// Kill clicks to internal links
if ((data.axis && ((data.axisX && movedY) || (data.axisY && movedX))) || (movedX || movedY)) {
data.$links = data.$el.find("a");
for (var i = 0, count = data.$links.length; i < count; i++) {
bindLink(data.$links.eq(i), data);
}
}
var newE = buildEvent(eType, e, newX, newY, data.scaleD, deltaX, deltaY, dirX, dirY);
$Window.off([
Events.touchMove,
Events.touchEnd,
Events.touchCancel,
Events.mouseMove,
Events.mouseUp,
Events.pointerMove,
Events.pointerUp,
Events.pointerCancel
].join(" "));
data.$el.trigger(newE);
data.touches = [];
if (data.scale) {
/*
if (e.originalEvent.pointerId) {
for (var i in data.touches) {
if (data.touches[i].id === e.originalEvent.pointerId) {
data.touches.splice(i, 1);
}
}
} else {
data.touches = e.originalEvent.touches;
}
*/
/*
if (data.touches.length) {
onPointerStart($.extend(e, {
data: data,
originalEvent: {
touches: data.touches
}
}));
}
*/
}
if (touch) {
data.touchTimer = Functions.startTimer(data.touchTimer, 5, function() {
data.$el.on(Events.mouseDown, data, onPointerStart);
});
}
data.touching = false;
}
/**
* @method private
* @name bindLink
* @description Bind events to internal links
* @param $link [object] "Object to bind"
* @param data [object] "Instance data"
*/
function bindLink($link, data) {
$link.on(Events.click, data, onLinkClick);
// http://www.elijahmanor.com/how-to-access-jquerys-internal-data/
var events = $._data($link[0], "events")["click"];
events.unshift(events.pop());
}
/**
* @method private
* @name onLinkClick
* @description Handles clicks to internal links
* @param e [object] "Event data"
*/
function onLinkClick(e) {
Functions.killEvent(e, true);
e.data.$links.off(Events.click);
}
/**
* @method private
* @name buildEvents
* @description Builds new event.
* @param type [type] "Event type"
* @param oe [object] "Original event"
* @param x [int] "X value"
* @param y [int] "Y value"
* @param scale [float] "Scale value"
* @param dx [float] "Delta X value"
* @param dy [float] "Delta Y value"
*/
function buildEvent(type, oe, px, py, s, dx, dy, dirx, diry) {
return $.Event(type, {
originalEvent: oe,
bubbles: true,
pageX: px,
pageY: py,
scale: s,
deltaX: dx,
deltaY: dy,
directionX: dirx,
directionY: diry
});
}
/**
* @method private
* @name midpoint
* @description Calculates midpoint.
* @param a [float] "Value 1"
* @param b [float] "Value 2"
*/
function midpoint(a, b) {
return (a + b) / 2.0;
}
/**
* @method private
* @name pythagorus
* @description Pythagorean theorem.
* @param a [float] "Value 1"
* @param b [float] "Value 2"
*/
function pythagorus(a, b) {
return Math.sqrt((a * a) + (b * b));
}
/**
* @method private
* @name touchAction
* @description Set ms touch action on target.
* @param action [string] "Touch action value"
*/
function touchAction($target, action) {
$target.css({
"-ms-touch-action": action,
"touch-action": action
});
}
/**
* @plugin
* @name Touch
* @description A jQuery plugin for multi-touch events.
* @type widget
* @main touch.js
* @dependency jQuery
* @dependency core.js
*/
var legacyPointer = !(Formstone.window.PointerEvent),
Plugin = Formstone.Plugin("touch", {
widget: true,
/**
* @options
* @param axis [string] <null> "Limit axis for pan and swipe; 'x' or 'y'"
* @param pan [boolean] <false> "Pan events"
* @param scale [boolean] <false> "Scale events"
* @param swipe [boolean] <false> "Swipe events"
*/
defaults: {
axis: false,
pan: false,
scale: false,
swipe: false
},
methods: {
_construct: construct,
_destruct: destruct
},
events: {
pointerDown: legacyPointer ? "MSPointerDown" : "pointerdown",
pointerUp: legacyPointer ? "MSPointerUp" : "pointerup",
pointerMove: legacyPointer ? "MSPointerMove" : "pointermove",
pointerCancel: legacyPointer ? "MSPointerCancel" : "pointercancel"
}
}),
// Localize References
Events = Plugin.events,
Functions = Plugin.functions,
// Local
$Window = Formstone.$window,
TouchThreshold = 10,
TouchTime = 50;
/**
* @events
* @event panstart "Panning started"
* @event pan "Panning"
* @event panend "Panning ended"
* @event scalestart "Scaling started"
* @event scale "Scaling"
* @event scaleend "Scaling ended"
* @event swipe "Swipe"
*/
Events.pan = "pan";
Events.panStart = "panstart";
Events.panEnd = "panend";
Events.scale = "scale";
Events.scaleStart = "scalestart";
Events.scaleEnd = "scaleend";
Events.swipe = "swipe";
})
);