UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

962 lines (857 loc) 29.4 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2014 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Christopher Zuendorf (czuendorf) * Daniel Wagner (danielwagner) ************************************************************************ */ /** * Listens for (native or synthetic) pointer events and fires events * for gestures like "tap" or "swipe" */ qx.Bootstrap.define("qx.event.handler.GestureCore", { extend: Object, implement: [qx.core.IDisposable], statics: { TYPES: [ "tap", "swipe", "longtap", "dbltap", "track", "trackstart", "trackend", "rotate", "pinch", "roll" ], GESTURE_EVENTS: [ "gesturebegin", "gesturefinish", "gesturemove", "gesturecancel" ], /** @type {Map} Maximum distance between a pointer-down and pointer-up event, values are configurable */ TAP_MAX_DISTANCE: { touch: 40, mouse: 5, pen: 20 }, // values are educated guesses /** @type {Map} Maximum distance between two subsequent taps, values are configurable */ DOUBLETAP_MAX_DISTANCE: { touch: 10, mouse: 4, pen: 10 }, // values are educated guesses /** @type {Map} The direction of a swipe relative to the axis */ SWIPE_DIRECTION: { x: ["left", "right"], y: ["up", "down"] }, /** * @type {Integer} The time delta in milliseconds to fire a long tap event. */ LONGTAP_TIME: qx.core.Environment.get("device.touch") ? 500 : 99999, /** * @type {Integer} Maximum time between two tap events that will still trigger a * dbltap event. */ DOUBLETAP_TIME: 500, /** * @type {Integer} Factor which is used for adapting the delta of the mouse wheel * event to the roll events, */ ROLL_FACTOR: 18, /** * @type {Integer} Factor which is used for adapting the delta of the touchpad gesture * event to the roll events, */ TOUCHPAD_ROLL_FACTOR: 1, /** * @type {Integer} Minimum number of wheel events to receive during the * TOUCHPAD_WHEEL_EVENTS_PERIOD to detect a touchpad. */ TOUCHPAD_WHEEL_EVENTS_THRESHOLD: 10, /** * @type {Integer} Period (in ms) during which the wheel events are counted in order * to detect a touchpad. */ TOUCHPAD_WHEEL_EVENTS_PERIOD: 100, /** * @type {Integer} Timeout (in ms) after which the touchpad detection is reset if no wheel * events are received in the meantime. */ TOUCHPAD_WHEEL_EVENTS_TIMEOUT: 5000 }, /** * @param target {Element} DOM Element that should fire gesture events * @param emitter {qx.event.Emitter?} Event emitter (used if dispatchEvent * is not supported, e.g. in IE8) */ construct(target, emitter) { this.__defaultTarget = target; this.__emitter = emitter; this.__gesture = {}; this.__lastTap = {}; this.__stopMomentum = {}; this.__momentum = {}; this.__rollEvents = []; this._initObserver(); }, members: { __defaultTarget: null, __emitter: null, __gesture: null, __eventName: null, __primaryTarget: null, __isMultiPointerGesture: null, __initialAngle: null, __lastTap: null, __rollImpulseId: null, __stopMomentum: null, __initialDistance: null, __momentum: null, __rollEvents: null, __rollEventsCountStart: 0, __rollEventsCount: 0, __touchPadDetectionPerformed: false, __lastRollEventTime: 0, /** * Register pointer event listeners */ _initObserver() { qx.event.handler.GestureCore.GESTURE_EVENTS.forEach( function (gestureType) { qxWeb(this.__defaultTarget).on( gestureType, this.checkAndFireGesture, this ); }.bind(this) ); if ( qx.core.Environment.get("engine.name") == "mshtml" && qx.core.Environment.get("browser.documentmode") < 9 ) { qxWeb(this.__defaultTarget).on("dblclick", this._onDblClick, this); } // list to wheel events var data = qx.core.Environment.get("event.mousewheel"); qxWeb(data.target).on(data.type, this._fireRoll, this); }, /** * Remove native pointer event listeners. */ _stopObserver() { qx.event.handler.GestureCore.GESTURE_EVENTS.forEach( function (pointerType) { qxWeb(this.__defaultTarget).off( pointerType, this.checkAndFireGesture, this ); }.bind(this) ); if ( qx.core.Environment.get("engine.name") == "mshtml" && qx.core.Environment.get("browser.documentmode") < 9 ) { qxWeb(this.__defaultTarget).off("dblclick", this._onDblClick, this); } var data = qx.core.Environment.get("event.mousewheel"); qxWeb(data.target).off(data.type, this._fireRoll, this); }, /** * Checks if a gesture was made and fires the gesture event. * * @param domEvent {qx.event.type.Pointer} DOM event * @param type {String ? null} type of the event * @param target {Element ? null} event target */ checkAndFireGesture(domEvent, type, target) { if (!type) { type = domEvent.type; } if (!target) { target = qx.bom.Event.getTarget(domEvent); } if (type == "gesturebegin") { this.gestureBegin(domEvent, target); } else if (type == "gesturemove") { this.gestureMove(domEvent, target); } else if (type == "gesturefinish") { this.gestureFinish(domEvent, target); } else if (type == "gesturecancel") { this.gestureCancel(domEvent.pointerId); } }, /** * Helper method for gesture start. * * @param domEvent {Event} DOM event * @param target {Element} event target */ gestureBegin(domEvent, target) { if (this.__gesture[domEvent.pointerId]) { this.__stopLongTapTimer(this.__gesture[domEvent.pointerId]); delete this.__gesture[domEvent.pointerId]; } /* If the dom event's target or one of its ancestors have a gesture handler, we don't need to fire the gesture again since it bubbles. */ if (this._hasIntermediaryHandler(target)) { return; } this.__gesture[domEvent.pointerId] = { startTime: new Date().getTime(), lastEventTime: new Date().getTime(), startX: domEvent.clientX, startY: domEvent.clientY, clientX: domEvent.clientX, clientY: domEvent.clientY, velocityX: 0, velocityY: 0, target: target, isTap: true, isPrimary: domEvent.isPrimary, longTapTimer: window.setTimeout( this.__fireLongTap.bind(this, domEvent, target), qx.event.handler.GestureCore.LONGTAP_TIME ) }; if (domEvent.isPrimary) { this.__isMultiPointerGesture = false; this.__primaryTarget = target; this.__fireTrack("trackstart", domEvent, target); } else { this.__isMultiPointerGesture = true; if (Object.keys(this.__gesture).length === 2) { this.__initialAngle = this._calcAngle(); this.__initialDistance = this._calcDistance(); } } }, /** * Helper method for gesture move. * * @param domEvent {Event} DOM event * @param target {Element} event target */ gestureMove(domEvent, target) { var gesture = this.__gesture[domEvent.pointerId]; if (gesture) { var oldClientX = gesture.clientX; var oldClientY = gesture.clientY; gesture.clientX = domEvent.clientX; gesture.clientY = domEvent.clientY; gesture.lastEventTime = new Date().getTime(); if (oldClientX) { gesture.velocityX = gesture.clientX - oldClientX; } if (oldClientY) { gesture.velocityY = gesture.clientY - oldClientY; } if (Object.keys(this.__gesture).length === 2) { this.__fireRotate(domEvent, gesture.target); this.__firePinch(domEvent, gesture.target); } if (!this.__isMultiPointerGesture) { this.__fireTrack("track", domEvent, gesture.target); this._fireRoll(domEvent, "touch", gesture.target); } // abort long tap timer if the distance is too big if (gesture.isTap) { gesture.isTap = this._isBelowTapMaxDistance(domEvent); if (!gesture.isTap) { this.__stopLongTapTimer(gesture); } } } }, /** * Checks if a DOM element located between the target of a gesture * event and the element this handler is attached to has a gesture * handler of its own. * * @param target {Element} The gesture event's target * @return {Boolean} */ _hasIntermediaryHandler(target) { while (target && target !== this.__defaultTarget) { if (target.$$gestureHandler) { return true; } target = target.parentNode; } return false; }, /** * Helper method for gesture end. * * @param domEvent {Event} DOM event * @param target {Element} event target */ gestureFinish(domEvent, target) { // If no start position is available for this pointerup event, cancel gesture recognition. if (!this.__gesture[domEvent.pointerId]) { return; } var gesture = this.__gesture[domEvent.pointerId]; // delete the long tap this.__stopLongTapTimer(gesture); /* If the dom event's target or one of its ancestors have a gesture handler, we don't need to fire the gesture again since it bubbles. */ if (this._hasIntermediaryHandler(target)) { return; } // always start the roll impulse on the original target this.__handleRollImpulse( gesture.velocityX, gesture.velocityY, domEvent, gesture.target ); this.__fireTrack("trackend", domEvent, gesture.target); if (gesture.isTap) { if (target !== gesture.target) { delete this.__gesture[domEvent.pointerId]; return; } this._fireEvent(domEvent, "tap", domEvent.target || target); var isDblTap = false; if (Object.keys(this.__lastTap).length > 0) { // delete old tap entries var limit = Date.now() - qx.event.handler.GestureCore.DOUBLETAP_TIME; for (var time in this.__lastTap) { if (time < limit) { delete this.__lastTap[time]; } else { var lastTap = this.__lastTap[time]; var isBelowDoubleTapDistance = this.__isBelowDoubleTapDistance( lastTap.x, lastTap.y, domEvent.clientX, domEvent.clientY, domEvent.getPointerType() ); var isSameTarget = lastTap.target === (domEvent.target || target); var isSameButton = lastTap.button === domEvent.button; if (isBelowDoubleTapDistance && isSameButton && isSameTarget) { isDblTap = true; delete this.__lastTap[time]; this._fireEvent(domEvent, "dbltap", domEvent.target || target); } } } } if (!isDblTap) { this.__lastTap[Date.now()] = { x: domEvent.clientX, y: domEvent.clientY, target: domEvent.target || target, button: domEvent.button }; } } else if (!this._isBelowTapMaxDistance(domEvent)) { var swipe = this.__getSwipeGesture(domEvent, target); if (swipe) { domEvent.swipe = swipe; this._fireEvent(domEvent, "swipe", gesture.target || target); } } delete this.__gesture[domEvent.pointerId]; }, /** * Stops the momentum scrolling currently running. * * @param id {Integer} The timeoutId of a 'roll' event */ stopMomentum(id) { this.__stopMomentum[id] = true; }, /** * Cancels the gesture if running. * @param id {Number} The pointer Id. */ gestureCancel(id) { if (this.__gesture[id]) { this.__stopLongTapTimer(this.__gesture[id]); delete this.__gesture[id]; } if (this.__momentum[id]) { this.stopMomentum(this.__momentum[id]); delete this.__momentum[id]; } }, /** * Update the target of a running gesture. This is used in virtual widgets * when the DOM element changes. * * @param id {String} The pointer id. * @param target {Element} The new target element. * @internal */ updateGestureTarget(id, target) { this.__gesture[id].target = target; }, /** * Method which will be called recursively to provide a momentum scrolling. * @param deltaX {Number} The last offset in X direction * @param deltaY {Number} The last offset in Y direction * @param domEvent {Event} The original gesture event * @param target {Element} The target of the momentum roll events * @param time {Number ?} The time in ms between the last two calls */ __handleRollImpulse(deltaX, deltaY, domEvent, target, time) { var oldTimeoutId = domEvent.timeoutId; if (!time && this.__momentum[domEvent.pointerId]) { // new roll impulse started, stop the old one this.stopMomentum(this.__momentum[domEvent.pointerId]); } // do nothing if we don't need to scroll if ( (Math.abs(deltaY) < 1 && Math.abs(deltaX) < 1) || this.__stopMomentum[oldTimeoutId] || (this.getWindow && !this.getWindow()) ) { delete this.__stopMomentum[oldTimeoutId]; delete this.__momentum[domEvent.pointerId]; return; } if (!time) { time = 1; var startFactor = 2.8; deltaY = deltaY / startFactor; deltaX = deltaX / startFactor; } time += 0.0006; deltaY = deltaY / time; deltaX = deltaX / time; // set up a new timer with the new delta var timeoutId = qx.bom.AnimationFrame.request( qx.lang.Function.bind( function (deltaX, deltaY, domEvent, target, time) { this.__handleRollImpulse(deltaX, deltaY, domEvent, target, time); }, this, deltaX, deltaY, domEvent, target, time ) ); deltaX = Math.round(deltaX * 100) / 100; deltaY = Math.round(deltaY * 100) / 100; // scroll the desired new delta domEvent.delta = { x: -deltaX, y: -deltaY }; domEvent.momentum = true; domEvent.timeoutId = timeoutId; this.__momentum[domEvent.pointerId] = timeoutId; this._fireEvent(domEvent, "roll", domEvent.target || target); }, /** * Calculates the angle of the primary and secondary pointer. * @return {Number} the rotation angle of the 2 pointers. */ _calcAngle() { var pointerA = null; var pointerB = null; for (var pointerId in this.__gesture) { var gesture = this.__gesture[pointerId]; if (pointerA === null) { pointerA = gesture; } else { pointerB = gesture; } } var x = pointerA.clientX - pointerB.clientX; var y = pointerA.clientY - pointerB.clientY; return (360 + Math.atan2(y, x) * (180 / Math.PI)) % 360; }, /** * Calculates the scaling distance between two pointers. * @return {Number} the calculated distance. */ _calcDistance() { var pointerA = null; var pointerB = null; for (var pointerId in this.__gesture) { var gesture = this.__gesture[pointerId]; if (pointerA === null) { pointerA = gesture; } else { pointerB = gesture; } } var scale = Math.sqrt( Math.pow(pointerA.clientX - pointerB.clientX, 2) + Math.pow(pointerA.clientY - pointerB.clientY, 2) ); return scale; }, /** * Checks if the distance between the x/y coordinates of DOM event * exceeds TAP_MAX_DISTANCE and returns the result. * * @param domEvent {Event} The DOM event from the browser. * @return {Boolean|null} true if distance is below TAP_MAX_DISTANCE. */ _isBelowTapMaxDistance(domEvent) { var delta = this._getDeltaCoordinates(domEvent); var maxDistance = qx.event.handler.GestureCore.TAP_MAX_DISTANCE[ domEvent.getPointerType() ]; if (!delta) { return null; } return ( Math.abs(delta.x) <= maxDistance && Math.abs(delta.y) <= maxDistance ); }, /** * Checks if the distance between the x1/y1 and x2/y2 is * below the TAP_MAX_DISTANCE and returns the result. * * @param x1 {Number} The x position of point one. * @param y1 {Number} The y position of point one. * @param x2 {Number} The x position of point two. * @param y2 {Number} The y position of point two. * @param type {String} The pointer type e.g. "mouse" * @return {Boolean} <code>true</code>, if points are in range */ __isBelowDoubleTapDistance(x1, y1, x2, y2, type) { var clazz = qx.event.handler.GestureCore; var inX = Math.abs(x1 - x2) < clazz.DOUBLETAP_MAX_DISTANCE[type]; var inY = Math.abs(y1 - y2) < clazz.DOUBLETAP_MAX_DISTANCE[type]; return inX && inY; }, /** * Calculates the delta coordinates in relation to the position on <code>pointerstart</code> event. * @param domEvent {Event} The DOM event from the browser. * @return {Map} containing the deltaX as x, and deltaY as y. */ _getDeltaCoordinates(domEvent) { var gesture = this.__gesture[domEvent.pointerId]; if (!gesture) { return null; } var deltaX = domEvent.clientX - gesture.startX; var deltaY = domEvent.clientY - gesture.startY; var axis = "x"; if (Math.abs(deltaX / deltaY) < 1) { axis = "y"; } return { x: deltaX, y: deltaY, axis: axis }; }, /** * Fire a gesture event with the given parameters * * @param domEvent {Event} DOM event * @param type {String} type of the event * @param target {Element ? null} event target * @return {qx.Promise?} a promise, if one or more of the event handlers returned a promise */ _fireEvent(domEvent, type, target) { // The target may have been removed, e.g. menu hide on tap if (!this.__defaultTarget) { return; } var evt; if (qx.core.Environment.get("event.dispatchevent")) { evt = new qx.event.type.dom.Custom(type, domEvent, { bubbles: true, swipe: domEvent.swipe, scale: domEvent.scale, angle: domEvent.angle, delta: domEvent.delta, pointerType: domEvent.pointerType, momentum: domEvent.momentum }); return target.dispatchEvent(evt); } else if (this.__emitter) { evt = new qx.event.type.dom.Custom(type, domEvent, { target: this.__defaultTarget, currentTarget: this.__defaultTarget, srcElement: this.__defaultTarget, swipe: domEvent.swipe, scale: domEvent.scale, angle: domEvent.angle, delta: domEvent.delta, pointerType: domEvent.pointerType, momentum: domEvent.momentum }); this.__emitter.emit(type, domEvent); } }, /** * Fire "tap" and "dbltap" events after a native "dblclick" * event to fix IE 8's broken mouse event sequence. * * @param domEvent {Event} dblclick event */ _onDblClick(domEvent) { var target = qx.bom.Event.getTarget(domEvent); this._fireEvent(domEvent, "tap", target); this._fireEvent(domEvent, "dbltap", target); }, /** * Returns the swipe gesture when the user performed a swipe. * * @param domEvent {Event} DOM event * @param target {Element} event target * @return {Map|null} returns the swipe data when the user performed a swipe, null if the gesture was no swipe. */ __getSwipeGesture(domEvent, target) { var gesture = this.__gesture[domEvent.pointerId]; if (!gesture) { return null; } var clazz = qx.event.handler.GestureCore; var deltaCoordinates = this._getDeltaCoordinates(domEvent); var duration = new Date().getTime() - gesture.startTime; var axis = Math.abs(deltaCoordinates.x) >= Math.abs(deltaCoordinates.y) ? "x" : "y"; var distance = deltaCoordinates[axis]; var direction = clazz.SWIPE_DIRECTION[axis][distance < 0 ? 0 : 1]; var velocity = duration !== 0 ? distance / duration : 0; var swipe = { startTime: gesture.startTime, duration: duration, axis: axis, direction: direction, distance: distance, velocity: velocity }; return swipe; }, /** * Fires a track event. * * @param type {String} the track type * @param domEvent {Event} DOM event * @param target {Element} event target */ __fireTrack(type, domEvent, target) { domEvent.delta = this._getDeltaCoordinates(domEvent); this._fireEvent(domEvent, type, domEvent.target || target); }, /** * Fires a roll event. * * @param domEvent {Event} DOM event * @param target {Element} event target * @param rollFactor {Integer} the roll factor to apply */ __fireRollEvent(domEvent, target, rollFactor) { domEvent.delta = { x: qx.util.Wheel.getDelta(domEvent, "x") * rollFactor, y: qx.util.Wheel.getDelta(domEvent, "y") * rollFactor }; domEvent.delta.axis = Math.abs(domEvent.delta.x / domEvent.delta.y) < 1 ? "y" : "x"; domEvent.pointerType = "wheel"; this._fireEvent(domEvent, "roll", domEvent.target || target); }, /** * Triggers the adaptative roll scrolling. * * @param target {Element} event target */ __performAdaptativeRollScrolling(target) { var rollFactor = qx.event.handler.GestureCore.ROLL_FACTOR; if (qx.util.Wheel.IS_TOUCHPAD) { // The domEvent was generated by a touchpad rollFactor = qx.event.handler.GestureCore.TOUCHPAD_ROLL_FACTOR; } this.__lastRollEventTime = new Date().getTime(); var reLength = this.__rollEvents.length; for (var i = 0; i < reLength; i++) { var domEvent = this.__rollEvents[i]; this.__fireRollEvent(domEvent, target, rollFactor); } this.__rollEvents = []; }, /** * Ends touch pad detection process. */ __endTouchPadDetection() { if ( this.__rollEvents.length > qx.event.handler.GestureCore.TOUCHPAD_WHEEL_EVENTS_THRESHOLD ) { qx.util.Wheel.IS_TOUCHPAD = true; } else { qx.util.Wheel.IS_TOUCHPAD = false; } if (qx.core.Environment.get("qx.debug.touchpad.detection")) { qx.log.Logger.debug(this, "IS_TOUCHPAD : " + qx.util.Wheel.IS_TOUCHPAD); } this.__touchPadDetectionPerformed = true; }, /** * Is touchpad detection enabled ? Default implementation activates it only for Mac OS after Sierra (>= 10.12). * @return {boolean} true if touchpad detection should occur. * @internal */ _isTouchPadDetectionEnabled() { return ( qx.core.Environment.get("os.name") == "osx" && qx.core.Environment.get("os.version") >= 10.12 ); }, /** * Fires a roll event after determining the roll factor to apply. Mac OS Sierra (10.12+) * introduces a lot more wheel events fired from the trackpad, so the roll factor to be applied * has to be reduced in order to make the scrolling less sensitive. * * @param domEvent {Event} DOM event * @param type {String} The type of the dom event * @param target {Element} event target */ _fireRoll(domEvent, type, target) { var now; var detectionTimeout; if (domEvent.type === qx.core.Environment.get("event.mousewheel").type) { if (this._isTouchPadDetectionEnabled()) { now = new Date().getTime(); detectionTimeout = qx.event.handler.GestureCore.TOUCHPAD_WHEEL_EVENTS_TIMEOUT; if ( this.__lastRollEventTime > 0 && now - this.__lastRollEventTime > detectionTimeout ) { // The detection timeout was reached. A new detection step should occur. this.__touchPadDetectionPerformed = false; this.__rollEvents = []; this.__lastRollEventTime = 0; } if (!this.__touchPadDetectionPerformed) { // We are into a detection session. We count the events so that we can decide if // they were fired by a real mouse wheel or a touchpad. Just swallow them until the // detection period is over. if (this.__rollEvents.length === 0) { // detection starts this.__rollEventsCountStart = now; qx.event.Timer.once( function () { if (!this.__touchPadDetectionPerformed) { // There were not enough events during the TOUCHPAD_WHEEL_EVENTS_PERIOD to actually // trigger a scrolling. Trigger it manually. this.__endTouchPadDetection(); this.__performAdaptativeRollScrolling(target); } }, this, qx.event.handler.GestureCore.TOUCHPAD_WHEEL_EVENTS_PERIOD + 50 ); } this.__rollEvents.push(domEvent); this.__rollEventsCount++; if ( now - this.__rollEventsCountStart > qx.event.handler.GestureCore.TOUCHPAD_WHEEL_EVENTS_PERIOD ) { this.__endTouchPadDetection(); } } if (this.__touchPadDetectionPerformed) { if (this.__rollEvents.length === 0) { this.__rollEvents.push(domEvent); } // Detection is done. We can now decide the roll factor to apply to the delta. // Default to a real mouse wheel event as opposed to a touchpad one. this.__performAdaptativeRollScrolling(target); } } else { this.__fireRollEvent( domEvent, target, qx.event.handler.GestureCore.ROLL_FACTOR ); } } else { var gesture = this.__gesture[domEvent.pointerId]; domEvent.delta = { x: -gesture.velocityX, y: -gesture.velocityY, axis: Math.abs(gesture.velocityX / gesture.velocityY) < 1 ? "y" : "x" }; this._fireEvent(domEvent, "roll", domEvent.target || target); } }, /** * Fires a rotate event. * * @param domEvent {Event} DOM event * @param target {Element} event target */ __fireRotate(domEvent, target) { if (!domEvent.isPrimary) { var angle = this._calcAngle(); domEvent.angle = Math.round((angle - this.__initialAngle) % 360); this._fireEvent(domEvent, "rotate", this.__primaryTarget); } }, /** * Fires a pinch event. * * @param domEvent {Event} DOM event * @param target {Element} event target */ __firePinch(domEvent, target) { if (!domEvent.isPrimary) { var distance = this._calcDistance(); var scale = distance / this.__initialDistance; domEvent.scale = Math.round(scale * 100) / 100; this._fireEvent(domEvent, "pinch", this.__primaryTarget); } }, /** * Fires the long tap event. * * @param domEvent {Event} DOM event * @param target {Element} event target */ __fireLongTap(domEvent, target) { var gesture = this.__gesture[domEvent.pointerId]; if (gesture) { this._fireEvent(domEvent, "longtap", domEvent.target || target); gesture.longTapTimer = null; gesture.isTap = false; } }, /** * Stops the time for the long tap event. * @param gesture {Map} Data may representing the gesture. */ __stopLongTapTimer(gesture) { if (gesture.longTapTimer) { window.clearTimeout(gesture.longTapTimer); gesture.longTapTimer = null; } }, /** * Dispose the current instance */ dispose() { for (var gesture in this.__gesture) { this.__stopLongTapTimer(gesture); } this._stopObserver(); this.__defaultTarget = this.__emitter = null; } } });