@eclipse-scout/core
Version:
Eclipse Scout runtime
277 lines (246 loc) • 9.8 kB
text/typescript
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {arrays, Device, objects, Predicate, strings} from '../index';
function isTouchEvent(event: JQuery.Event): event is JQuery.TouchEventBase {
return event && strings.startsWith(event.type, 'touch');
}
export const events = {
/**
* @returns the x coordinate where the event happened, works for touch events as well.
*/
pageX(event: JQuery.TriggeredEvent): number {
if (!objects.isNullOrUndefined(event.pageX)) {
return event.pageX;
}
// @ts-expect-error
return event.originalEvent.touches[0].pageX;
},
/**
* @returns the y coordinate where the event happened, works for touch events as well.
*/
pageY(event: JQuery.TriggeredEvent): number {
if (!objects.isNullOrUndefined(event.pageY)) {
return event.pageY;
}
// @ts-expect-error
return event.originalEvent.touches[0].pageY;
},
touchdown(touch: boolean, suffix?: string): string {
return events.touchOrMouse(touch, 'touchstart', 'mousedown', suffix);
},
touchmove(touch: boolean, suffix?: string): string {
return events.touchOrMouse(touch, 'touchmove', 'mousemove', suffix);
},
touchendcancel(touch: boolean, suffix?: string): string {
return events.touchOrMouse(touch, 'touchend touchcancel', 'mouseup', suffix);
},
touchOrMouse(touch: boolean, touchEvent: string, mouseEvent: string, suffix?: string): string {
suffix = suffix || '';
if (suffix) {
suffix = '.' + suffix;
}
if (touch) {
return touchEvent + suffix;
}
return mouseEvent + suffix;
},
isTouchEvent,
fixTouchEvent(event: JQuery.Event) {
if (isTouchEvent(event)) {
let touches = event.touches || (event.originalEvent ? event.originalEvent.touches : null);
let touch = touches ? touches[0] : null;
if (touch) {
// Touch events may contain fractional values, while mouse events should not
// - https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageX
// - https://www.chromestatus.com/features/6169687914184704
event.pageX = Math.round(touch.pageX) as undefined;
event.pageY = Math.round(touch.pageY) as undefined;
}
}
},
/**
* @returns an object containing passive: true if the browser supports passive event listeners, otherwise returns false.
*/
passiveOptions(): boolean | { passive: boolean } {
let options: boolean | { passive: boolean } = false;
if (Device.get().supportsPassiveEventListener()) {
options = {
passive: true
};
}
return options;
},
/**
* Listens for scroll events and executes startHandler on first event. It then regularly checks for further scroll events and executes endHandler if no event has fired since a certain amount of time and the user has released his finger.
* If he does not release his finger the endHandler won't be called even if the pane has stopped scrolling.
*/
onScrollStartEndDuringTouch($elem: JQuery, startHandler: () => void, endHandler: () => void) {
let scrollTimeout;
let started = false;
let touchend = false;
let scrollHandler = event => {
// Execute once on first scroll event (and not as soon as user touches the pane because he might not even want to scroll)
if (!started) {
startHandler();
started = true;
}
clearTimeout(scrollTimeout);
// Check some ms later if a scroll event has occurred in the meantime.
// If yes it (probably) is still scrolling. If no it (probably) is not scrolling anymore -> call handler
checkLater();
};
let touchEndHandler = event => {
touchend = true;
checkLater();
};
function checkLater() {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
if (touchend) {
// Only stop processing if user released the finger
removeHandlers();
endHandler();
}
}, 50);
}
function removeHandlers() {
$elem.off('scroll', scrollHandler);
$elem.document(false).off('touchend touchcancel', touchEndHandler);
}
$elem.on('scroll', scrollHandler);
// Make sure handler is executed and scroll listener removed if no scroll event occurs
$elem.document(false).one('touchend touchcancel', touchEndHandler);
},
/**
* Forwards the event to the given target by creating a new event with the same data as the old one.
* Prevents default action of the original event if preventDefault was called for the forwarded event.
* Does not use jQuery to make sure the capture phase is executed as well.
*
* <p>
* <b>Important</b>
* This function only works in browsers supporting the Event constructor (e.g. KeyboardEvent: https://developer.mozilla.org/de/docs/Web/API/KeyboardEvent/KeyboardEvent).
* </p>
*
* @param target the element which should receive the event
* @param event the original event which should be propagated
*/
propagateEvent(target: EventTarget, event: Event) {
if (typeof (Event) !== 'function') {
return;
}
// @ts-expect-error
let newEvent: PropagatedEvent = new event.constructor(event.type, event);
newEvent.sourceEvent = event;
if (!target.dispatchEvent(newEvent)) {
event.preventDefault();
}
},
/**
* Adds an event listener for each given type to the source which propagates the events for that type to the target.
*
* <p>
* <b>Important</b>
* This function only works in browsers supporting the Event constructor (e.g. KeyboardEvent: https://developer.mozilla.org/de/docs/Web/API/KeyboardEvent/KeyboardEvent).
* </p>
*
* @param source the element for which the event listener should be added.
* @param target the element which should receive the event.
* @param types an array of event types.
* @param an optional filter function which can return false if the event should not be propagated.
*/
addPropagationListener(source: EventTarget, target: EventTarget, types: string[], filter?: Predicate<Event>) {
types = arrays.ensure(types);
types.forEach(type => {
source.addEventListener(type, event => {
if (filter && !filter(event)) {
return;
}
events.propagateEvent(target, event);
});
});
},
/**
* Adds swipe event listeners to the element given.
*
* @param $element The element on which the listeners should be attached.
* @param id An event listener id used to be registered on the window object.
* @param [onDown] Callback to be invoked when the swipe is started (mouse or touch down).
* @param [onMove] Callback to be invoked when mouse (or finger if touch) is moved (while being down).
* @param [onUp] Callback to be invoked when the swipe is ended (mouse or finger released).
*/
onSwipe($element: JQuery, id: string, onDown?: (event: SwipeCallbackEvent) => boolean, onMove?: (event: SwipeCallbackEvent) => number, onUp?: (event: SwipeCallbackEvent) => void) {
let $window = $element.window(false);
let touch = Device.get().supportsOnlyTouch();
$element.on('touchmove', event => event.preventDefault()); // prevent scrolling the background when swiping (iOS)
$element.on('remove', event => $window.off('.' + id));
$element.on(events.touchdown(touch), event => {
let origPosLeft = $element.position().left;
let acceptDown = !onDown || !!onDown({originalEvent: event, originalLeft: origPosLeft, deltaX: 0, newLeft: origPosLeft, direction: 0});
if (!acceptDown) {
return;
}
let dragging = true;
let origPageX = events.pageX(event);
let curPosLeft = origPosLeft;
let direction = 0;
$window.on(events.touchmove(touch, id), event => {
let pageX = events.pageX(event);
let deltaX = pageX - origPageX;
let newLeft = origPosLeft + deltaX;
if (newLeft !== curPosLeft) {
// only update swipe direction if it actually changed
direction = Math.sign(newLeft - curPosLeft);
}
if (onMove) {
let l = onMove({originalEvent: event, originalLeft: origPosLeft, deltaX: deltaX, newLeft: newLeft, direction: direction});
curPosLeft = typeof l === 'number' ? l : newLeft;
} else {
curPosLeft = newLeft;
}
});
$window.on(events.touchendcancel(touch, id), event => {
if (!dragging) {
// On iOS touchcancel and touchend are fired right after each other when swiping twice very fast -> Ignore the second event
return;
}
dragging = false;
$window.off('.' + id);
if (onUp) {
onUp({originalEvent: event, originalLeft: origPosLeft, deltaX: curPosLeft - origPosLeft, newLeft: curPosLeft, direction: direction});
}
});
});
}
};
export interface SwipeCallbackEvent {
/**
* The original event received from the browser.
*/
originalEvent: JQuery.TriggeredEvent;
/**
* The left position of the element at the moment the swipe was started.
*/
originalLeft: number;
/**
* The horizontal delta the swipe has already moved (negative values mean to the left of the original left position).
*/
deltaX: number;
/**
* The current left position of the element.
*/
newLeft: number;
/**
* -1 if the move is to the left, 1 if the move is to the right, 0 or -0 if it is not moved yet
*/
direction: number;
}
export interface PropagatedEvent extends Event {
sourceEvent: Event;
}