zk/src/main/resources/web/js/zk/domtouch.ts
/* domtouch.ts
Purpose:
Enhance/fix ios dom event
Description:
History:
Wedi Mar 30 15:14:49 2011, Created by jimmyshiau
Copyright (C) 2011 Potix Corporation. All Rights Reserved.
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
*/
function _createMouseEvent (type: string, button: number, changedTouch: Touch, ofs = {sx: 0, sy: 0, cx: 0, cy: 0}): MouseEvent {
var simulatedEvent = document.createEvent('MouseEvent');
simulatedEvent.initMouseEvent(type, true, true, window, 1,
changedTouch.screenX + ofs.sx, changedTouch.screenY + ofs.sy,
changedTouch.clientX + ofs.cx, changedTouch.clientY + ofs.cy,
false, false, false, false, button, null); // eslint-disable-line zk/noNull
return simulatedEvent;
}
function _createJQEvent(target: Element, type: string, button: number, changedTouch: Touch): JQuery.Event {
//do not allow text
//ZK-1011
if (target && (target.nodeType === 3 || target.nodeType === 8))
target = target.parentNode as Element;
var originalEvent = _createMouseEvent(type, button, changedTouch),
props: string[] = [], // ZK-4565, jq.event.props is removed in jquery 3.5.0
event = jq.Event(originalEvent);
//Add missing props removed by jQuery
props.push('button', 'charCode', 'clientX', 'clientY', 'detail',
'fromElement', 'keyCode', 'layerX', 'layerY', 'offsetX', 'offsetY',
'pageX', 'pageY', 'screenX', 'screenY', 'srcElement', 'toElement');
for (var i = props.length, prop: string; i;) {
prop = props[--i];
event[prop] = originalEvent[prop] as unknown;
}
event.target = target;
return event;
}
function _toMouseEvent(event: JQuery.TouchEventBase, changedTouch: Touch): JQuery.Event | undefined {
switch (event.type) {
case 'touchstart':
return _createJQEvent(changedTouch.target as HTMLElement, 'mousedown', 0, changedTouch);
case 'touchend':
return _createJQEvent(
document.elementFromPoint(
changedTouch.clientX,
changedTouch.clientY) as HTMLElement,
'mouseup', 0, changedTouch);
case 'touchmove':
var ele = document.elementFromPoint(changedTouch.clientX, changedTouch.clientY);
return (ele && _createJQEvent(ele, 'mousemove', 0, changedTouch)) || undefined;
}
return event;
}
function _doEvt(type: string, evt: JQuery.TouchEventBase, jqevt: JQuery.Event): void {
var eventFuncs = jq.data(evt.currentTarget as never, 'zk_eventFuncs') as Record<string, CallableFunction[]>,
typeLabel = _findEventTypeLabel(type, eventFuncs),
funcs: CallableFunction[];
//store original event for invoke stop
jqevt['touchEvent'] = evt.originalEvent;
if (eventFuncs && typeLabel && (funcs = eventFuncs[typeLabel])) {
for (var i = 0, l = funcs.length; i < l; i++)
funcs[i](jqevt);
}
}
function delegateEventFunc (event: JQuery.TouchEventBase): void {
var touchEvt = event.originalEvent as (JQuery.Event|TouchEvent) & {sourceCapabilities?: object},
touches = touchEvt.touches,
sourceCapabilities = touchEvt.sourceCapabilities;
if (touches && touches.length > 1) return;
else if (touches && touches.length == 1) {
zk.currentPointer = [touches['clientX'], touches['clientY']] as zk.Offset;
}
if (touchEvt instanceof MouseEvent
&& sourceCapabilities && sourceCapabilities['firesTouchEvents']) return; // handled by touch handler
var evt: JQuery.Event | undefined,
changedTouches = touchEvt.changedTouches ? touchEvt.changedTouches[0] : undefined;
if ((evt = _toMouseEvent(event, changedTouches!)))
_doEvt(event.type, event, evt);
}
zk.copy(zjq.eventTypes, {
zmousedown: 'touchstart mousedown',
zmouseup: 'touchend mouseup',
zmousemove: 'touchmove mousemove'
});
function _findEventTypeLabel(type: string, eventFuncs: Record<string, unknown>): string | undefined {
var exactType = eventFuncs[type];
if (exactType)
return type;
var evtTypes = Object.keys(eventFuncs);
for (var i = 0, length = evtTypes.length; i < length; i++) {
var val = evtTypes[i];
if (val.indexOf(type) !== -1)
return val;
}
return undefined;
}
function _storeEventFunction(elem: Element, type: string, data: unknown, fn: CallableFunction): boolean {
var eventFuncs = jq.data(elem, 'zk_eventFuncs') as Record<string, CallableFunction[]>,
funcs: CallableFunction[];
//store functions in jq data
if (!eventFuncs) {
eventFuncs = {};
jq.data(elem, 'zk_eventFuncs', eventFuncs);
}
if ((funcs = eventFuncs[type])) {
funcs.push(fn);
return false; //already listen
}
eventFuncs[type] = [fn];
return true;
}
function _removeEventFunction(elem: Element, type: string, fn: CallableFunction): boolean {
var eventFuncs = jq.data(elem, 'zk_eventFuncs') as Record<string, CallableFunction[]>,
funcs: CallableFunction[];
if (eventFuncs && (funcs = eventFuncs[type])) {
funcs.$remove(fn);
if (!funcs.length) {
delete eventFuncs[type];
for (var i in eventFuncs)
if (i)
return true;
jq.removeData(elem, 'zk_eventFuncs');
return true; //has no listen
}
}
return false;
}
const _xWidget: Partial<zk.Widget> = {};
zk.override(zk.Widget.prototype, _xWidget, {
_swipe: <zk.Swipe | undefined> undefined,
_startTap: <((wgt) => void) | undefined> undefined,
_holdTime: 0,
_rightClickPending: false,
_holdTimeout: <number | undefined> undefined,
_rightClickEvent: <JQuery.Event | undefined> undefined,
_tapValid: false,
_tapTimeout: <number | undefined> undefined,
_lastTap: <Element | undefined> undefined,
_dbTap: false,
_startHold: <((evt: JQuery.TouchEventBase) => void) | undefined> undefined,
_cancelHold: <CallableFunction | undefined> undefined,
_pt: [0, 0],
_cancelMouseUp: false,
_cancelClick: <number | undefined> undefined,
/** @internal */
bindSwipe_() {
var node = this.$n() as HTMLElement;
if (this.isListen('onSwipe') || jq(node).data('swipeable'))
this._swipe = new zk.Swipe(this, node);
},
/** @internal */
unbindSwipe_() {
var swipe = this._swipe;
if (swipe) {
this._swipe = undefined;
swipe.destroy(this.$n() as HTMLElement);
}
},
/** @internal */
bindDoubleTap_() {
if (this.isListen('onDoubleClick')) {
var doubleClickTime = 500;
this._startTap = (wgt: typeof this) => {
wgt._lastTap = wgt.$n() as HTMLElement; //Holds last tapped element (so we can compare for double tap)
wgt._tapValid = true; //Are we still in the .5 second window where a double tap can occur
wgt._tapTimeout = setTimeout(function () {
wgt._tapValid = false;
}, doubleClickTime);
};
jq(this.$n()).on('touchstart', this.proxy(this._dblTapStart))
.on('touchend', this.proxy(this._dblTapEnd));
}
},
/** @internal */
unbindDoubleTap_() {
if (this.isListen('onDoubleClick')) {
this._startTap = undefined;
jq(this.$n()).off('touchstart', this.proxy(this._dblTapStart))
.off('touchend', this.proxy(this._dblTapEnd));
}
},
/** @internal */
_dblTapStart(evt: JQuery.TouchStartEvent) {
var tevt = evt.originalEvent as TouchEvent;
if (tevt.touches.length > 1) return;
if (!this._tapValid) {
this._startTap!(this);
} else {
clearTimeout(this._tapTimeout);
this._tapTimeout = undefined;
if (this.$n() == this._lastTap) {
this._dbTap = true;
} else {
this._startTap!(this);
}
}
// ZK-2179: should skip row widget
var p = (zk.Widget.$(evt.target) as zk.Widget).parent;
if (p && (!zk.isLoaded('zul.grid') || !(p instanceof zul.grid.Row))
&& (!zk.isLoaded('zul.sel') || (!(p instanceof zul.sel.Listitem) && !(p instanceof zul.sel.Treerow))))
tevt.stopPropagation();
},
/** @internal */
_dblTapEnd(evt: JQuery.TouchEndEvent) {
var tevt = evt.originalEvent as TouchEvent;
if (tevt.touches.length > 1) return;
if (this._dbTap) {
this._dbTap = this._tapValid = false;
this._lastTap = undefined;
var changedTouch = tevt.changedTouches[0],
wevt = new zk.Event(this, 'onDoubleClick', {pageX: changedTouch.clientX, pageY: changedTouch.clientY}, {}, evt);
if (!this.$weave) {
if (!wevt.stopped)
this.doDoubleClick_(wevt);
if (wevt.domStopped)
wevt.domEvent!.stop();
}
tevt.preventDefault(); //stop ios zoom
}
},
/** @internal */
bindTapHold_() {
if (this.isListen('onRightClick') || (window.zul && this instanceof zul.Widget && this.getContext())) { //also register context menu to tapHold event
this._holdTime = 800;
this._startHold = (evt: JQuery.TouchEventBase): void => {
if (!this._rightClickPending) {
var self = this;
self._rightClickPending = true; // We could be performing a right click
self._rightClickEvent = evt;
self._holdTimeout = setTimeout(function () {
self._rightClickPending = false;
var evt = self._rightClickEvent,
wevt = new zk.Event(self, 'onRightClick', {pageX: self._pt[0], pageY: self._pt[1]}, {}, evt as never);
if (!self.$weave) {
if (!wevt.stopped)
self.doRightClick_(wevt);
if (wevt.domStopped)
wevt.domEvent!.stop();
}
// Note: I don't mouse up the right click here however feel free to add if required
self._cancelMouseUp = true;
self._rightClickEvent = undefined;
}, self._holdTime);
}
};
this._cancelHold = () => {
if (this._rightClickPending) {
this._rightClickPending = false;
this._rightClickEvent = undefined;
clearTimeout(this._holdTimeout);
this._holdTimeout = undefined;
}
};
jq(this.$n()).on('touchstart', this.proxy(this._tapHoldStart))
.on('touchmove', this.proxy(this._tapHoldMove)) //cancel hold if moved
.on('click', this.proxy(this._tapHoldClick)) //prevent click during hold
.on('touchend', this.proxy(this._tapHoldEnd));
}
},
/** @internal */
unbindTapHold_() {
if (this.isListen('onRightClick') || (window.zul && this instanceof zul.Widget && this.getContext())) { //also register context menu to tapHold event
this._startHold = this._cancelHold = undefined;
jq(this.$n()).off('touchstart', this.proxy(this._tapHoldStart))
.off('touchmove', this.proxy(this._tapHoldMove)) //cancel hold if moved
.off('click', this.proxy(this._tapHoldClick)) //prevent click during hold
.off('touchend', this.proxy(this._tapHoldEnd));
}
},
/** @internal */
_tapHoldStart(evt: JQuery.TouchEventBase) {
var tevt = evt.originalEvent as TouchEvent;
if (tevt.touches.length > 1)
return;
var changedTouch = tevt.changedTouches[0];
this._pt = [changedTouch.clientX, changedTouch.clientY];
this._startHold!(evt);
// ZK-2179: should skip row widget
var p = (zk.Widget.$(evt.target) as zk.Widget).parent;
if (p && (!zk.isLoaded('zul.grid') || !(p instanceof zul.grid.Row))
&& (!zk.isLoaded('zul.sel') || (!(p instanceof zul.sel.Listitem) && !(p instanceof zul.sel.Treerow))))
tevt.stopPropagation();
},
/** @internal */
_tapHoldMove(evt: JQuery.TouchEventBase) {
var tevt = evt.originalEvent as TouchEvent,
initSensitivity = 3;
if (tevt.touches.length > 1 || !this._pt)
return;
var changedTouch = tevt.changedTouches[0];
if (Math.abs(changedTouch.clientX - this._pt[0]) > initSensitivity
|| Math.abs(changedTouch.clientY - this._pt[1]) > initSensitivity)
this._cancelHold!();
},
/** @internal */
_tapHoldClick(evt: JQuery.TouchEventBase) {
if (this._cancelClick) {
//stop click after hold
if ((Date.now() - this._cancelClick) < 100) {
evt.stopImmediatePropagation();
return false;
}
this._cancelClick = undefined;
}
},
/** @internal */
_tapHoldEnd(evt: JQuery.TouchEventBase) {
var tevt = evt.originalEvent as TouchEvent;
if (tevt.touches.length > 1) return;
if (this._cancelMouseUp) {
this._cancelClick = jq.now();
this._cancelMouseUp = false;
evt.stopImmediatePropagation();
return false;
}
this._cancelHold!();
}
});
var _jqEvent: Partial<JQuery.Event> = {},
_jqEventSpecial = {};
zk.augment(jq.fn, {
// @ts-expect-error: incompatible with the signature in the original jQuery
on: function (this: JQuery, type: string, selector, data: unknown, fn: unknown, ...rest: unknown[]) {
var evtType: string | undefined;
if ((evtType = zjq.eventTypes[type])) {
// refer to jquery on function for reassign args
if (data == null && fn == null) {
// ( type, fn )
fn = selector;
data = selector = undefined;
} else if (fn == null) {
if (typeof selector === 'string') {
// ( type, selector, fn )
fn = data;
data = undefined;
} else {
// ( type, data, fn )
fn = data;
data = selector;
selector = undefined;
}
}
if (_storeEventFunction(this[0], evtType, data, fn as CallableFunction))
this.zon(evtType, selector as string, data, delegateEventFunc);
} else {
this.zon(type, selector as string, data, fn as CallableFunction, ...rest); // Bug ZK-1142: recover back to latest domios.js
}
return this;
},
// @ts-expect-error: incompatible with the signature in the original jQuery
off: function (this: JQuery, type: string, selector: unknown, fn: unknown, ...rest: unknown[]) {
var evtType: string | undefined;
if ((evtType = zjq.eventTypes[type])) {
// refer to jquery on function for reassign args
if (selector === false || typeof selector === 'function') {
// ( type [, fn] )
fn = selector;
selector = undefined;
}
if (_removeEventFunction(this[0], evtType, fn as never))
this.zoff(evtType, selector as string, delegateEventFunc);
} else {
this.zoff(type, selector as string, fn as never, ...rest); // Bug ZK-1142: recover back to latest domios.js
}
return this;
}
});
zk.override(jq.Event.prototype as JQuery.Event, _jqEvent, {
touchEvent: <TouchEvent | undefined> undefined,
stop() {
_jqEvent.stop!.bind(this as JQuery.Event)();
var tEvt: TouchEvent | undefined;
if ((tEvt = this.touchEvent)) {
if (tEvt.cancelable) tEvt.preventDefault();
tEvt.stopPropagation();
}
}
});
zk.override(jq.event.special, _jqEventSpecial, {
touchstart: {
setup(this: EventTarget, data, namespaces, eventHandle: EventListenerOrEventListenerObject) {
this.addEventListener('touchstart', eventHandle, {passive: false}); // ZK-4678
}
},
touchmove: {
setup: function (this: EventTarget, data, namespaces, eventHandle: EventListenerOrEventListenerObject) {
this.addEventListener('touchmove', eventHandle, {passive: false}); // ZK-4678
}
}
});