Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
always listen to mouse + touch events (#305)
(* add polyfill for PointerEvents simplify listening to events on different browsers with(-out) touch support) * merge conflicts resolved * refactoring of event handling, listen to mouse and touch events but make sure that they are not applied twice for the same user action * reformat indentation * add EventHandler to test-main * fix touchmove event handling
- Loading branch information
1 parent
e598468
commit 27cb8ac
Showing
4 changed files
with
307 additions
and
247 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,294 @@ | ||
define([], function() { | ||
|
||
/** | ||
* General handler for all mouse and touch actions. | ||
* | ||
* The general flow of mouse actions are: | ||
* 1. mousedown - "button pressed" | ||
* 2. mouseout - "button released" | ||
* 3. mouseout (mouse moved inside again) - "button pressed" | ||
* 4. mouseup - "button released" | ||
* | ||
* 2. gets mapped to a action cancel event | ||
* 3. gets mapped to a mousedown event | ||
* 2. and 3. can be repeated unlimited - or also be left out. | ||
* 4. triggers the real action | ||
* | ||
* For touch it's a little different as a touchmove cancels the current | ||
* action and translates into a scroll. | ||
* | ||
* All of this is the default or when the mousemove callback is returning | ||
* restrict=true (or undefined). | ||
* When restrict=false the widget captures the mouse until it is released. | ||
*/ | ||
function EventHandler(templateEngine) { | ||
|
||
this._navbarRegEx = /navbar/; | ||
this._isTouchDevice = !!('ontouchstart' in window) || // works on most browsers | ||
!!('onmsgesturechange' in window); // works on ie10 | ||
this._isWidget = false; | ||
this._scrollElement = null; | ||
// object to hold the coordinated of the current mouse / touch event | ||
this._mouseEvent = templateEngine.handleMouseEvent = { | ||
moveFn: undefined, | ||
moveRestrict: true, | ||
actor: undefined, | ||
widget: undefined, | ||
widgetCreator: undefined, | ||
downtime: 0, | ||
alreadyCanceled: false | ||
}; | ||
this._touchStartX = null; | ||
this._touchStartY = null; | ||
|
||
// helper function to get the current actor and widget out of an event: | ||
this.getWidgetActor = function (element) { | ||
var actor, widget; | ||
|
||
while (element) { | ||
if (element.classList.contains('actor') || (element.classList.contains('group') && element.classList.contains('clickable'))) { | ||
actor = element; | ||
} | ||
|
||
if (element.classList.contains('widget_container')) { | ||
widget = element; | ||
if (templateEngine.design.creators[widget.dataset.type].action !== undefined) { | ||
return {actor: actor, widget: widget}; | ||
} | ||
} | ||
if (element.classList.contains('page')) { | ||
// abort traversal | ||
return {actor: actor, widget: widget}; | ||
} | ||
element = element.parentElement; | ||
} | ||
return false; | ||
}; | ||
|
||
// helper function to determine the element to scroll (or undefined) | ||
this.getScrollElement = function (element) { | ||
while (element) { | ||
if (element.classList.contains('page')) { | ||
return this._navbarRegEx.test(element.id) ? undefined : element; | ||
} | ||
if (element.classList.contains('navbar')) { | ||
var parent = element.parentElement; | ||
if ('navbarTop' === parent.id || 'navbarBottom' === parent.id) { | ||
return element; | ||
} | ||
return; | ||
} | ||
|
||
element = element.parentElement; | ||
} | ||
}; | ||
|
||
this.onPointerDown = function (event) { | ||
// search if a widget was hit | ||
var widgetActor = this.getWidgetActor(event.target), | ||
bindWidget = widgetActor.widget ? templateEngine.widgetDataGet(widgetActor.widget.id).bind_click_to_widget : false; | ||
|
||
var touchobj; | ||
|
||
if (event.changedTouches) { | ||
touchobj = event.changedTouches[0]; | ||
this._touchStartX = parseInt(touchobj.clientX); | ||
this._touchStartY = parseInt(touchobj.clientY); | ||
} else { | ||
this._touchStartX = parseInt(event.clientX); | ||
this._touchStartY = parseInt(event.clientY); | ||
} | ||
|
||
this._isWidget = widgetActor.widget !== undefined && (bindWidget || widgetActor.actor !== undefined); | ||
if (this._isWidget) { | ||
this._mouseEvent.actor = widgetActor.actor; | ||
this._mouseEvent.widget = widgetActor.widget; | ||
this._mouseEvent.widgetCreator = templateEngine.design.creators[widgetActor.widget.dataset.type]; | ||
this._mouseEvent.downtime = Date.now(); | ||
this._mouseEvent.alreadyCanceled = false; | ||
|
||
var actionFn = this._mouseEvent.widgetCreator.downaction; | ||
|
||
if (actionFn !== undefined) { | ||
var moveFnInfo = actionFn.call(this._mouseEvent.widget, this._mouseEvent.widget.id, this._mouseEvent.actor, false, event); | ||
if (moveFnInfo) { | ||
this._mouseEvent.moveFn = moveFnInfo.callback; | ||
this._mouseEvent.moveRestrict = moveFnInfo.restrict !== undefined ? moveFnInfo.restrict : true; | ||
} | ||
} | ||
} else { | ||
this._mouseEvent.actor = undefined; | ||
} | ||
|
||
if (this._mouseEvent.moveRestrict) { | ||
this._scrollElement = this.getScrollElement(event.target); | ||
} | ||
// stop the propagation if scrollable is at the end | ||
// inspired by | ||
if (this._scrollElement) { | ||
var startTopScroll = this._scrollElement.scrollTop; | ||
|
||
if (startTopScroll <= 0) { | ||
this._scrollElement.scrollTop = 1; | ||
} | ||
if (startTopScroll + this._scrollElement.offsetHeight >= this._scrollElement.scrollHeight) { | ||
this._scrollElement.scrollTop = this._scrollElement.scrollHeight - this._scrollElement.offsetHeight - 1; | ||
} | ||
} | ||
}; | ||
|
||
this.onPointerUp = function (event) { | ||
if (this._isWidget) { | ||
var | ||
widgetActor = this.getWidgetActor(event.target), | ||
widget = this._mouseEvent.widget, | ||
actionFn = this._mouseEvent.widgetCreator.action, | ||
bindWidget = templateEngine.widgetDataGet(widget.id).bind_click_to_widget, | ||
inCurrent = widgetActor.widget === widget && (bindWidget || widgetActor.actor === this._mouseEvent.actor); | ||
|
||
if ( | ||
actionFn !== undefined && | ||
inCurrent && !this._mouseEvent.alreadyCanceled | ||
) { | ||
actionFn.call(widget, widget.id, this._mouseEvent.actor, !inCurrent, event); | ||
} | ||
this._mouseEvent.moveFn = undefined; | ||
this._mouseEvent.moveRestrict = true; | ||
this._scrollElement = undefined; | ||
this._isWidget = false; | ||
} | ||
}; | ||
|
||
/** | ||
* mouse move: let the user cancel an action by dragging the mouse outside | ||
* and reactivate it when the dragged cursor is returning | ||
* @param event {Event} | ||
* @private | ||
*/ | ||
this._onPointerMoveNoTouch = function (event) { | ||
if (this._isWidget) { | ||
var | ||
actionFn = null, | ||
widgetActor = this.getWidgetActor(event.target), | ||
widget = this._mouseEvent.widget, | ||
bindWidget = templateEngine.widgetDataGet(widget.id).bind_click_to_widget, | ||
inCurrent = !this._mouseEvent.moveRestrict || (widgetActor.widget === widget && (bindWidget || widgetActor.actor === this._mouseEvent.actor)); | ||
|
||
if (inCurrent && this._mouseEvent.moveFn) { | ||
this._mouseEvent.moveFn(event); | ||
} | ||
|
||
if (inCurrent && this._mouseEvent.alreadyCanceled) { // reactivate | ||
this._mouseEvent.alreadyCanceled = false; | ||
actionFn = this._mouseEvent.widgetCreator.downaction; | ||
if (actionFn) { | ||
actionFn.call(widget, widget.id, this._mouseEvent.actor, false, event); | ||
} | ||
} | ||
else if ((!inCurrent && !this._mouseEvent.alreadyCanceled)) { | ||
// cancel | ||
this._mouseEvent.alreadyCanceled = true; | ||
actionFn = this._mouseEvent.widgetCreator.action; | ||
if (actionFn) { | ||
actionFn.call(widget, widget.id, this._mouseEvent.actor, true, event); | ||
} | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* touch move: scroll when the finger is moving and cancel any pending | ||
* actions at the same time | ||
* @private | ||
*/ | ||
this._onPointerMoveTouch = function (event) { | ||
if (this._isWidget) { | ||
var | ||
widget = this._mouseEvent.widget, | ||
touchobj = event.changedTouches[0]; | ||
|
||
if (this._mouseEvent.moveFn) { | ||
this._mouseEvent.moveFn(event); | ||
} | ||
// cancel when finger moved more than 5px | ||
if (this._mouseEvent.moveRestrict && !this._mouseEvent.alreadyCanceled && | ||
(Math.abs(this._touchStartX - parseInt(touchobj.clientX)) > 5 || | ||
Math.abs(this._touchStartY - parseInt(touchobj.clientY)) > 5 )) { // cancel | ||
this._mouseEvent.alreadyCanceled = true; | ||
var actionFn = this._mouseEvent.widgetCreator.action; | ||
if (actionFn) { | ||
actionFn.call(widget, widget.id, this._mouseEvent.actor, true, event); | ||
} | ||
} | ||
} | ||
|
||
// take care to prevent overscroll | ||
if (this._scrollElement) { | ||
var scrollTop = this._scrollElement.scrollTop, | ||
scrollLeft = this._scrollElement.scrollLeft; | ||
// prevent scrolling of an element that takes full height and width | ||
// as it doesn't need scrolling | ||
if ((scrollTop <= 0) && (scrollTop + this._scrollElement.offsetHeight >= this._scrollElement.scrollHeight) && | ||
(scrollLeft <= 0) && (scrollLeft + this._scrollElement.offsetWidth >= this._scrollElement.scrollWidth )) { | ||
return; | ||
} | ||
event.stopPropagation(); | ||
} else { | ||
event.preventDefault(); | ||
} | ||
}; | ||
|
||
/** | ||
* The dispatcher registers listeners for all relevant events to the window object | ||
* and dispatched the event to the EventHandler. The dispatcher listens to similar events | ||
* like touchstart and mousedown but makes sure that these events are not fired | ||
* | ||
* @param handler | ||
* @constructor | ||
*/ | ||
var Dispatcher = function(handler) { | ||
|
||
/** | ||
* register to all events | ||
*/ | ||
this.register = function() { | ||
window.addEventListener('mousedown', this._onDown); | ||
window.addEventListener('touchstart', this._onDown); | ||
|
||
window.addEventListener('mouseup', this._onUp); | ||
window.addEventListener('touchend', this._onUp); | ||
|
||
window.addEventListener('mousemove', this._onMove); | ||
window.addEventListener('touchmove', this._onMove); | ||
}; | ||
|
||
this._onDown = function(event) { | ||
handler.onPointerDown(event); | ||
}; | ||
|
||
this._onUp = function(event) { | ||
handler.onPointerUp(event); | ||
if (event.type === "touchend") { | ||
// prevent mouseup beeing fired | ||
event.preventDefault(); | ||
} | ||
}; | ||
|
||
this._onMove = function(event) { | ||
// dispatch by event type | ||
if (event.type === "mousemove") { | ||
handler._onPointerMoveNoTouch(event); | ||
} else if (event.type === "touchmove") { | ||
handler._onPointerMoveTouch(event); | ||
} else{ | ||
console.error("onhandled event type "+event.type); | ||
} | ||
}; | ||
}; | ||
|
||
this.dispatcher = new Dispatcher(this); | ||
this.dispatcher.register(); | ||
|
||
} | ||
return EventHandler; | ||
}); |
Oops, something went wrong.