diff --git a/api/js/egw_action/egw_action_popup.js b/api/js/egw_action/egw_action_popup.js index a697f317b9..888bba7d69 100644 --- a/api/js/egw_action/egw_action_popup.js +++ b/api/js/egw_action/egw_action_popup.js @@ -18,6 +18,7 @@ import {egwAction, egwActionImplementation, egwActionObject} from './egw_action. import {egwFnct} from './egw_action_common.js'; import {egwMenu, _egw_active_menu} from "./egw_menu.js"; import {EGW_KEY_ENTER, EGW_KEY_MENU} from "./egw_action_constants.js"; +import {tapAndSwipe} from "../tapandswipe"; if (typeof window._egwActionClasses == "undefined") window._egwActionClasses = {}; @@ -280,35 +281,23 @@ export function egwPopupActionImplementation() }; ai._handleTapHold = function (_node, _callback) { - let holdTimer = 600; - let maxDistanceAllowed = 40; - let tapTimeout = null; - let startx = 0; - let starty = 0; - //TODO (todo-jquery): ATM we need to convert the possible given jquery dom node object into DOM Element, this // should be no longer neccessary after removing jQuery nodes. if (_node instanceof jQuery) { _node = _node[0]; } - _node.addEventListener('touchstart', function(e){ - tapTimeout = setTimeout(function(event){ - _callback(e); - }, holdTimer); - startx = (e.changedTouches) ? e.changedTouches[0].pageX: e.pageX; - starty = (e.changedTouches) ? e.changedTouches[0].pageY: e.pageY; - }); - _node.addEventListener('touchend', function(){ - clearTimeout(tapTimeout); - }); - _node.addEventListener('touchmove', function(_event){ - if (tapTimeout == null) return; - let e = _event.originalEvent; - let x = (e.changedTouches) ? e.changedTouches[0].pageX: e.pageX; - let y = (e.changedTouches) ? e.changedTouches[0].pageY: e.pageY; - if (Math.sqrt((x-startx)*(x-startx) + (y-starty)+(y-starty)) > maxDistanceAllowed) clearTimeout(tapTimeout); + let tap = new tapAndSwipe(_node, { + // this threshold must be the same as the one set in et2_dataview_view_aoi + tapHoldThreshold: 600, + tapAndHold: function(event) + { + // don't trigger contextmenu if sorting is happening + if (document.querySelector('.sortable-drag')) return; + + _callback(event); + } }); } /** @@ -350,7 +339,7 @@ export function egwPopupActionImplementation() // Safari still needs the taphold to trigger contextmenu // Chrome has default event on touch and hold which acts like right click this._handleTapHold(_node, contextHandler); - jQuery(_node).on('contextmenu', contextHandler); + if (!egwIsMobile()) jQuery(_node).on('contextmenu', contextHandler); }; ai.doRegisterAction = function(_aoi, _callback, _context) diff --git a/api/js/etemplate/et2_dataview_view_aoi.ts b/api/js/etemplate/et2_dataview_view_aoi.ts index f6e0fde487..4b625a1696 100644 --- a/api/js/etemplate/et2_dataview_view_aoi.ts +++ b/api/js/etemplate/et2_dataview_view_aoi.ts @@ -23,6 +23,7 @@ import {EGW_AO_SHIFT_STATE_MULTI, EGW_AO_STATE_SELECTED} from '../egw_action/egw_action_constants.js'; import {egwBitIsSet, egwGetShiftState, egwPreventSelect, egwSetBit, egwUnfocus, egwIsMobile} from "../egw_action/egw_action_common.js"; import {_egw_active_menu} from "../egw_action/egw_menu.js"; +import {tapAndSwipe} from "../tapandswipe"; /** * Contains the action object interface implementation for the nextmatch widget @@ -114,23 +115,21 @@ export function et2_dataview_rowAOI(_node) }; if (egwIsMobile()) { - jQuery(_node).swipe({ - allowPageScroll: "vertical", - longTapThreshold: 10, - swipe: function (event, direction, distance) - { - if (distance > 100) selectHandler(event, {swip:direction}); - }, - tap: function (event, duration) - { - selectHandler(event); - }, - // stop scrolling touch being confused from tap - longTap: function (event) - { - return; - } - + let swipe = new tapAndSwipe(_node,{ + // set the same threshold as action_popup event to get the tapAndHold working + tapHoldThreshold: 600, + swipe: function (event, direction, distance) + { + if (distance > 100) selectHandler(event, {swip:direction}); + }, + tap: function (event) + { + selectHandler(event); + }, + tapAndHold: function(event) + { + return; + } }); } else { jQuery(_node).click(selectHandler); diff --git a/api/js/tapandswipe.ts b/api/js/tapandswipe.ts new file mode 100644 index 0000000000..3a27283bf7 --- /dev/null +++ b/api/js/tapandswipe.ts @@ -0,0 +1,179 @@ +/** + * EGroupware TapAndSwipe helper library + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package Api + * @subpackage ui + * @link https://www.egroupware.org + * @author Hadi Nategh + */ +export interface TapAndSwipeOptions { + // tolorated pixel to fire the swipe events + threshold? : number, + // time delay for defirentiate between tap event and long tap event, threshold is in milliseconds + tapHoldThreshold? : number, + // callback function being called on swipe gestures + swipe? : Function, + // callback function being called on tap + tap? : Function, + // callback function being called on long tap(tap and hold) + tapAndHold? : Function +} + +export type TapAndSwipeOptionsType = TapAndSwipeOptions; + +export class tapAndSwipe { + static readonly _default : TapAndSwipeOptionsType = { + threshold : 10, + tapHoldThreshold : 3000, + swipe : function(){}, + tap : function(){}, + tapAndHold: function(){} + } + /** + * Keeps the touch X start point + * @private + */ + private _startX : number = null; + /** + * Keeps the touch Y start point + * @private + */ + private _startY : number = null; + /** + * Keeps the touch X end point + * @private + */ + private _endX : number = null; + /** + * Keeps the touch Y end point + * @private + */ + private _endY : number = null; + /** + * keeps the distance travelled between start point and end point + * @private + */ + private _distance : number = null; + /** + * flag to keep the status of type of tap + * @private + */ + private _isTapAndHold : boolean = false; + /** + * keeps the timeout id for taphold + */ + private _tapHoldTimeout : number = null; + /** + * Options + * @protected + */ + protected options : TapAndSwipeOptionsType = null; + /** + * Keeps the html node + */ + private _element : HTMLElement = null; + + /** + * Constructor + * @param _element + * @param _options + */ + public constructor(_element : string | HTMLElement, _options? : TapAndSwipeOptionsType) + { + this.options = {...tapAndSwipe._default, ..._options}; + this._element = (_element instanceof EventTarget) ? _element : document.querySelector(_element); + this._element.addEventListener('touchstart', this._onTouchStart.bind(this), false); + this._element.addEventListener('touchend', this._ontouchEnd.bind(this), false); + } + + /** + * on touch start event handler + * @param event + * @private + */ + private _onTouchStart(event : TouchEvent) + { + this._startX = event.changedTouches[0].screenX; + this._startY = event.changedTouches[0].screenY; + this._isTapAndHold = false; + + this._tapHoldTimeout = window.setTimeout(_=>{ + this._isTapAndHold = true; + this.options.tapAndHold(event); + }, this.options.tapHoldThreshold); + } + + /** + * On touch end event handler + * @param event + * @private + */ + private _ontouchEnd(event : TouchEvent) + { + this._endX = event.changedTouches[0].screenX; + this._endY = event.changedTouches[0].screenY; + this._handler(event); + } + + /** + * Handles the type of gesture and calls the right callback for it + * @param event + * @private + */ + private _handler(event : TouchEvent) + { + //cleanup tapHoldTimeout + window.clearTimeout(this._tapHoldTimeout); + + // Tap & TapAndHold handler + if (this._endX == this._startX && this._endY == this._startY + || (Math.sqrt((this._endX-this._startX)*(this._endX-this._startX) + (this._endY-this._startY)+(this._endY-this._startY)) + < this.options.threshold)) + { + if (!this._isTapAndHold) + { + this.options.tap(event); + } + + return; + } + + // left swipe handler + if (this._endX + this.options.threshold < this._startX) { + this._distance = this._startX - this._endX; + this.options.swipe(event, 'left', this._distance); + return; + } + + // right swipe handler + if (this._endX - this.options.threshold > this._startX) { + this._distance = this._endX - this._startX; + this.options.swipe(event, 'right', this._distance); + return; + } + + // up swipe handler + if (this._endY + this.options.threshold < this._startY) { + this._distance = this._startY - this._endY; + this.options.swipe(event, 'up', this._distance); + return; + } + + // down swipe handler + if (this._endY - this.options.threshold > this._startY) { + this._distance = this._endY - this._startY; + this.options.swipe(event, 'down', this._distance); + return; + } + } + + /** + * destroy the event listeners + */ + public destroy() + { + this._element.removeEventListener('touchstart', this._onTouchStart); + this._element.removeEventListener('touchend', this._ontouchEnd); + } +} \ No newline at end of file