/* Siesta 5.1.0 Copyright(c) 2009-2018 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ Role('Siesta.Test.Simulate.Touch', { requires : [ ], has: { touchEventNamesMap : { lazy : 'this.buildTouchEventNamesMap' }, currentTouchId : 1, activeTouches : Joose.I.Object, longPressDelay : { init : 1500, is : 'rw' }, forceTouchEvents : bowser.chrome }, methods: { simulateTap : function (context, options) { var queue = new Siesta.Util.Queue({ deferer : this.test.originalSetTimeout, deferClearer : this.test.originalClearTimeout, interval : 30, observeTest : this.test }) var me = this; var id queue.addStep({ processor : function () { id = me.touchStart(null, null, options, context) } }) queue.addStep({ processor : function () { me.touchEnd(id, options) } }) return new Promise(function (resolve, reject) { queue.run(resolve) }) }, simulateDoubleTap : function (context, options) { var queue = new Siesta.Util.Queue({ deferer : this.test.originalSetTimeout, deferClearer : this.test.originalClearTimeout, interval : 30, observeTest : this.test }) var me = this; var id queue.addStep({ processor : function () { id = me.touchStart(null, null, options, context) } }) queue.addStep({ processor : function () { me.touchEnd(id, options) } }) queue.addStep({ processor : function () { id = me.touchStart(null, null, options, context) } }) queue.addStep({ processor : function () { me.touchEnd(id, options) } }) return new Promise(function (resolve, reject) { queue.run(resolve) }) }, simulateLongPress : function (context, options) { var queue = new Siesta.Util.Queue({ deferer : this.test.originalSetTimeout, deferClearer : this.test.originalClearTimeout, interval : 30, observeTest : this.test }) var me = this; var id queue.addStep({ processor : function () { id = me.touchStart(null, null, options, context) } }) queue.addDelayStep(this.getLongPressDelay()) queue.addStep({ processor : function () { me.touchEnd(id, options) } }) return new Promise(function (resolve, reject) { queue.run(resolve) }) }, simulatePinch : function (context1, context2, options) { var queue = new Siesta.Util.Queue({ deferer : this.test.originalSetTimeout, deferClearer : this.test.originalClearTimeout, interval : 30, observeTest : this.test }) var id1, id2 var dx = context1.localXY[ 0 ] - context2.localXY[ 0 ] var dy = context1.localXY[ 1 ] - context2.localXY[ 1 ] var distance = Math.sqrt(dx * dx + dy * dy) if (distance < 1) distance = 1 var scaled = distance * scale var delta = (scaled - distance) / 2 var angle = Math.atan(dy / dx) var x1 = Math.round(context1.localXY[ 0 ] - delta * Math.cos(angle)) var y1 = Math.round(context1.localXY[ 1 ] - delta * Math.sin(angle)) var x2 = Math.round(context2.localXY[ 0 ] + delta * Math.cos(angle)) var y2 = Math.round(context2.localXY[ 1 ] + delta * Math.sin(angle)) var options2 = Joose.O.extend({}, options) queue.addStep({ processor : function () { id1 = me.touchStart(null, null, options, context1) id2 = me.touchStart(null, null, options2, context2) } }) queue.addAsyncStep({ processor : function (data) { var move1Done = false var move2Done = false me.touchMove(id1, x1, y1, function () { move1Done = true if (move1Done && move2Done) data.next() }, null, options) me.touchMove(id2, x2, y2, function () { move2Done = true if (move1Done && move2Done) data.next() }, null, options2) } }) queue.addStep({ processor : function () { me.touchEnd(id1, options) me.touchEnd(id2, options2) } }) return new Promise(function (resolve, reject) { queue.run(resolve) }) }, simulateTouchDrag : function (sourceXY, targetXY, options, dragOnly) { var me = this options = options || {}; // For drag operations we should always use the top level document.elementFromPoint var source = me.elementFromPoint(sourceXY[ 0 ], sourceXY[ 1 ], true); var target = me.elementFromPoint(targetXY[ 0 ], targetXY[ 1 ], true); var queue = new Siesta.Util.Queue({ deferer : this.test.originalSetTimeout, deferClearer : this.test.originalClearTimeout, interval : me.dragDelay, callbackDelay : me.afterActionDelay, observeTest : this.test }); var id queue.addStep({ processor : function () { id = me.touchStart(sourceXY, null, options) } }) queue.addAsyncStep({ processor : function (data) { me.touchMove(id, targetXY[ 0 ], targetXY[ 1 ], options).then(data.next) } }) queue.addStep({ processor : function () { // if `dragOnly` flag is set, do not finalize the touch, instead, pass the touch id // to the user in the callback (see below) if (!dragOnly) me.touchEnd(id, options) } }) return new Promise(function (resolve, reject) { queue.run(function () { // if `dragOnly` flag is set pass the touch id as promise result if (dragOnly) resolve(id) else resolve() }) }) }, touchStart : function (target, offset, options, context) { if (!context) context = this.test.getNormalizedTopElementInfo(target, true, 'touchStart', offset) options = Joose.O.extend({ clientX : context.localXY[ 0 ], clientY : context.localXY[ 1 ] }, options || {}) var event = this.simulateTouchEventGeneric(context.el, 'start', options) return event.pointerId || event.changedTouches[ 0 ].identifier }, touchEnd : function (touchId, options) { var touch = this.activeTouches[ touchId ] if (!touch) throw "Can't find active touch: " + touchId options = Joose.O.extend({ clientX : touch.clientX, clientY : touch.clientY }, options || {}) var target = touch.target if (!this.test.isInDom(target)) { touch.target = this.global.document.body } this.simulateTouchEventGeneric(touch.currentEl || touch.target, 'end', options, { touchId : touchId }) }, touchMove : function (touchId, toX, toY, options) { var touch = this.activeTouches[ touchId ] if (!touch) throw "Can't find active touch: " + touchId var me = this var overEls = [] return this.movePointerTemplate({ xy : [ touch.clientX, touch.clientY ], xy2 : [ toX, toY ], options : options || {}, overEls : overEls, interval : me.dragDelay, callbackDelay : me.afterActionDelay, pathBatchSize : me.pathBatchSize, onVoidOverEls : function () { return overEls = [] }, onPointerEnter : function (el, options) { }, onPointerLeave : function (el, options) { }, onPointerOver : function (el, options) { }, onPointerOut : function (el, options) { }, onPointerMove : function (el, options) { touch.clientX = options.clientX touch.clientY = options.clientY touch.pageX = me.viewportXtoPageX(options.clientX) touch.pageY = me.viewportYtoPageY(options.clientY) touch.currentEl = el me.simulateTouchEventGeneric(el, 'move', options, { touchId : touchId }) } }) }, // never used yet, should be called when touchMove goes out of the document touchCancel : function (touchId, options) { var touch = this.activeTouches[ touchId ] if (!touch) throw "Can't find active touch: " + touchId this.simulateTouchEventGeneric(touch.currentEl || touch.target, 'cancel', options, { touchId : touchId }) }, simulatePointerEventModern : function (target, type, options, simOptions) { target = this.test.normalizeElement(target) if (!target) return false var supports = Siesta.Project.Browser.FeatureSupport().supports options = options || {} if (/pointerdown$/i.test(type) && (!("clientX" in options) || !("clientY" in options))) { var center = this.test.findCenter(target); options = Joose.O.extend({ clientX : center[ 0 ], clientY : center[ 1 ] }, options) } var doc = this.global.document var event = new PointerEvent(type, { bubbles : true, cancelable : true, view : this.global, detail : options.detail, screenX : options.screenX, screenY : options.screenY, clientX : options.clientX, clientY : options.clientY, ctrlKey : options.ctrlKey || false, altKey : options.altKey || false, shiftKey : options.shiftKey || false, metaKey : options.metaKey || false, button : options.button, relatedTarget : options.relatedTarget || doc.documentElement, pointerId : simOptions.touchId || this.currentTouchId++, // pointerType // NOTE: this has to be set to "mouse" (IE11) or 4 (IE10, 11) because otherwise // ExtJS5 blocks the event // need to investigate what happens in SenchaTouch pointerType : 4 //mouse }) target.dispatchEvent(event) return event }, simulatePointerEvent : function (target, type, options, simOptions) { var supports = Siesta.Project.Browser.FeatureSupport().supports if (supports.PointerEvents && this.bowser.chrome) return this.simulatePointerEventModern(target, type, options, simOptions) target = this.test.normalizeElement(target) if (!target) return false options = options || {} var doc = this.global.document, event = doc.createEvent( supports.PointerEvents ? 'PointerEvent' : supports.MSPointerEvents ? 'MSPointerEvent' : 'MouseEvents' ) if (/pointerdown$/i.test(type) && (!("clientX" in options) || !("clientY" in options))) { var center = this.test.findCenter(target); options = Joose.O.extend({ clientX : center[ 0 ], clientY : center[ 1 ] }, options) } event[ (supports.MSPointerEvents || supports.PointerEvents) ? 'initPointerEvent' : 'initMouseEvent' ]( type, true, true, this.global, options.detail, options.screenX, options.screenY, options.clientX, options.clientY, options.ctrlKey || false, options.altKey || false, options.shiftKey || false, options.metaKey || false, options.button, options.relatedTarget || doc.documentElement, // the following extra args are used in the "initPointerEvent" // offsetX, offsetY null, null, // width, height null, null, // pressure, rotation null, null, // tiltX, tiltY null, null, // pointerId simOptions.touchId || this.currentTouchId++, // pointerType // NOTE: this has to be set to "mouse" (IE11) or 4 (IE10, 11) because otherwise // ExtJS5 blocks the event // need to investigate what happens in SenchaTouch 4,//'mouse', // timestamp, isPrimary null, null ); if (!(supports.MSPointerEvents || supports.PointerEvents)) { event.pointerId = simOptions.touchId || this.currentTouchId++ } target.dispatchEvent(event) return event }, simulateTouchEvent : function (target, type, options, simOptions) { options = options || {} var global = this.global var doc = global.document var event = new global.CustomEvent(type, { bubbles : true, cancelable : true }) var target = this.test.normalizeElement(target) var clientX, clientY if (("clientX" in options) && ("clientY" in options)) { clientX = options.clientX clientY = options.clientY } else { var center = this.test.findCenter(target); clientX = center[ 0 ] clientY = center[ 1 ] } var activeTouches = this.activeTouches var touch = simOptions.touch var touches = [] var targetTouches = [] for (var id in activeTouches) { var currentTouch = activeTouches[ id ] touches.push(currentTouch) if (currentTouch.target == target) targetTouches.push(currentTouch) } Joose.O.extend(event, { target : target, changedTouches : this.createTouchList([ touch ]), touches : this.createTouchList(touches), targetTouches : this.createTouchList(targetTouches), altKey : options.altKey, metaKey : options.metaKey, ctrlKey : options.ctrlKey, shiftKey : options.shiftKey }); target.dispatchEvent(event) return event }, createTouchList : function (touchList) { var doc = this.global.document // a branch for browsers supporting "createTouch/createTouchList" if (doc.createTouch) { var touches = []; for (var i = 0; i < touchList.length; i++) { var touchCfg = touchList[ i ]; touches.push(doc.createTouch( doc.defaultView || doc.parentWindow, touchCfg.target, touchCfg.identifier || this.currentTouchId++, touchCfg.pageX, touchCfg.pageY, touchCfg.screenX || touchCfg.pageX, touchCfg.screenY || touchCfg.pageY, touchCfg.clientX, touchCfg.clientY )) } return doc.createTouchList.apply(doc, touches); } else return touchList }, createTouch: function (target, clientX, clientY) { return { identifier : this.currentTouchId++, target : target, clientX : clientX, clientY : clientY, screenX : 0, screenY : 0, // TODO should take scrolling into account pageX : clientX, pageY : clientY } }, buildTouchEventNamesMap : function () { var supports = Siesta.Project.Browser.FeatureSupport().supports return supports.PointerEvents && !this.forceTouchEvents ? { start : 'pointerdown', move : 'pointermove', end : 'pointerup', cancel : 'pointercancel' } : supports.MSPointerEvents ? { start : 'MSPointerDown', move : 'MSPointerMove', end : 'MSPointerUp', cancel : 'MSPointerCancel' } : /*supports.TouchEvents ?*/ { start : 'touchstart', move : 'touchmove', end : 'touchend', cancel : 'touchcancel' } // : // // todo: fire mouseevents? // (function () { throw "Touch events not supported" })() }, simulateTouchEventGeneric : function (target, type, options, simOptions) { simOptions = simOptions || {} var target = this.test.normalizeElement(target) var clientX, clientY if (("clientX" in options) && ("clientY" in options)) { clientX = options.clientX clientY = options.clientY } else { var center = this.test.findCenter(target); clientX = center[ 0 ] clientY = center[ 1 ] } var activeTouches = this.activeTouches var touch if (type === 'end' || type === 'cancel') { touch = activeTouches[ simOptions.touchId ] target = touch.currentEl || touch.target delete activeTouches[ simOptions.touchId ] } else if (type == 'start') { touch = this.createTouch(target, clientX, clientY) activeTouches[ touch.identifier ] = touch } else if (type == 'move') { touch = activeTouches[ simOptions.touchId ] // "*move" events should be fired only from the "movePointerTemplate" method // which provides the "clientX/clientY" properties touch.clientX = options.clientX touch.clientY = options.clientY } if (!touch) throw "Can't find active touch" + (simOptions.touchId ? ': ' + simOptions.touchId : '') if (!simOptions.touchId) simOptions.touchId = touch.identifier simOptions.touch = touch var eventType = this.getTouchEventNamesMap()[ type ] var supports = Siesta.Project.Browser.FeatureSupport().supports if ((supports.PointerEvents || supports.MSPointerEvents) && !this.forceTouchEvents) { return this.simulatePointerEvent(target, eventType, options, simOptions) } else /*if (supports.TouchEvents)*/ { return this.simulateTouchEvent(target, eventType, options, simOptions); } // } else { // // TODO fallback to mouse events? // throw "Can't simulate any type of touch events" // } } } });