add dom events into support lib

This commit is contained in:
Rene
2020-11-12 23:55:06 +01:00
parent 84fbb436f9
commit 4987652d9d
9 changed files with 299 additions and 48 deletions
@@ -1,4 +1,18 @@
import { createDOM, style, appendChildren, offsetSize, scrollLeft, scrollTop, jsAPI, each, prependChildren, removeElements } from 'support';
import {
createDOM,
style,
appendChildren,
offsetSize,
scrollLeft,
scrollTop,
jsAPI,
runEach,
prependChildren,
removeElements,
on,
preventDefault,
stopPropagation,
} from 'support';
import { Environment } from 'environment';
const animationStartEventName = 'animationstart';
@@ -14,10 +28,8 @@ const rAF = requestAnimationFrame;
const getDirection = (elm: HTMLElement) => style(elm, 'direction');
// TODO:
// 1. handling for event listeners (animationStartEventName.split(' '))
// 2. return not just element but also destruction function
// 3. shorthand handling for preventDefault & stopPropagation etc.
// 5. MAYBE add comparison function to offsetSize etc.
// 1. MAYBE add comparison function to offsetSize etc.
// 2. remove supportPassiveListeners & resizeobserver from environment
export const createSizeObserver = (
target: HTMLElement,
@@ -37,6 +49,7 @@ export const createSizeObserver = (
}
onSizeChangedCallback(dir === true);
};
const offListeners: (() => void)[] = [];
let appearCallback: (...args: any) => any = onSizeChangedCallbackProxy;
if (ResizeObserverConstructor) {
@@ -81,14 +94,14 @@ export const createSizeObserver = (
reset();
if (scrollEvent) {
scrollEvent.preventDefault();
scrollEvent.stopPropagation();
preventDefault(scrollEvent);
stopPropagation(scrollEvent);
}
return false;
};
expandElement.addEventListener(scrollEventName, onScroll);
shrinkElement.addEventListener(scrollEventName, onScroll);
offListeners.push(on(expandElement, scrollEventName, onScroll));
offListeners.push(on(shrinkElement, scrollEventName, onScroll));
// lets assume that the divs will never be that large and a constant value is enough
style(expandElementChild, {
@@ -99,36 +112,34 @@ export const createSizeObserver = (
appearCallback = onScroll;
}
each(animationStartEventName.split(' '), (eventName) => {
sizeObserver.addEventListener(eventName, () => {
appearCallback();
});
});
if (direction) {
let dirCache: string | undefined;
sizeObserver.addEventListener('scroll', (event: Event) => {
const dir = getDirection(sizeObserver);
const changed = dir !== dirCache;
if (changed) {
if (dir === 'rtl') {
style(listenerElement, { left: 'auto', right: 0 });
} else {
style(listenerElement, { left: 0, right: 'auto' });
offListeners.push(
on(sizeObserver, scrollEventName, (event: Event) => {
const dir = getDirection(sizeObserver);
const changed = dir !== dirCache;
if (changed) {
if (dir === 'rtl') {
style(listenerElement, { left: 'auto', right: 0 });
} else {
style(listenerElement, { left: 0, right: 'auto' });
}
dirCache = dir;
onSizeChangedCallbackProxy(true);
}
dirCache = dir;
onSizeChangedCallbackProxy(true);
}
event.preventDefault();
event.stopPropagation();
return false;
});
preventDefault(event);
stopPropagation(event);
return false;
})
);
}
offListeners.push(on(sizeObserver, animationStartEventName, appearCallback));
prependChildren(target, sizeObserver);
return () => {
runEach(offListeners);
removeElements(sizeObserver);
};
};
@@ -1,4 +1,7 @@
import { WH } from 'support/dom';
export interface WH<T = number> {
w: T;
h: T;
}
const elementHasDimensions = (elm: HTMLElement): boolean => !!(elm.offsetWidth || elm.offsetHeight || elm.getClientRects().length);
const zeroObj: WH = {
@@ -1,6 +1,89 @@
export const on = (target: EventTarget, type: string, listener: EventListenerOrEventListenerObject | null, options: AddEventListenerOptions): void => {
import { each, runEach } from 'support/utils/array';
let passiveEventsSupport: boolean;
const supportPassiveEvents = (): boolean => {
if (passiveEventsSupport === undefined) {
passiveEventsSupport = false;
try {
/* eslint-disable */
// @ts-ignore
window.addEventListener(
'test',
null,
Object.defineProperty({}, 'passive', {
get: function () {
passiveEventsSupport = true;
},
})
);
/* eslint-enable */
} catch (e) {}
}
return passiveEventsSupport;
};
target.addEventListener(type, listener, options);
};
export interface OnOptions {
_capture?: boolean;
_passive?: boolean;
_once?: boolean;
}
/**
* Removes the passed event listener for the passed events with the passed options.
* @param target The element from which the listener shall be removed.
* @param eventNames The eventsnames for which the listener shall be removed.
* @param listener The listener which shall be removed.
* @param capture The options of the removed listener.
*/
export const off = (target: EventTarget, eventNames: string, listener: EventListener, capture?: boolean): void => {
each(eventNames.split(' '), (eventName) => {
target.removeEventListener(eventName, listener, capture);
});
};
/**
* Adds the passed event listener for the passed eventnames with the passed options.
* @param target The element to which the listener shall be added.
* @param eventNames The eventsnames for which the listener shall be called.
* @param listener The listener which is called on the eventnames.
* @param options The options of the added listener.
*/
export const on = (target: EventTarget, eventNames: string, listener: EventListener, options?: OnOptions): (() => void) => {
const doSupportPassiveEvents = supportPassiveEvents();
const passive = (doSupportPassiveEvents && options && options._passive) || false;
const capture = (options && options._capture) || false;
const once = (options && options._once) || false;
const offListeners: (() => void)[] = [];
const nativeOptions: AddEventListenerOptions | boolean = doSupportPassiveEvents
? {
passive,
capture,
}
: capture;
each(eventNames.split(' '), (eventName) => {
const finalListener = once
? (evt: Event) => {
target.removeEventListener(eventName, finalListener, capture);
listener && listener(evt);
}
: listener;
offListeners.push(off.bind(null, target, eventName, finalListener, capture));
target.addEventListener(eventName, finalListener, nativeOptions);
});
return runEach.bind(0, offListeners);
};
/**
* Shorthand for the stopPropagation event Method.
* @param evt The event of which the stopPropagation method shall be called.
*/
export const stopPropagation = (evt: Event) => evt.stopPropagation();
/**
* Shorthand for the preventDefault event Method.
* @param evt The event of which the preventDefault method shall be called.
*/
export const preventDefault = (evt: Event) => evt.preventDefault();
@@ -2,17 +2,8 @@ export * from 'support/dom/attribute';
export * from 'support/dom/class';
export * from 'support/dom/create';
export * from 'support/dom/dimensions';
export * from 'support/dom/events';
export * from 'support/dom/style';
export * from 'support/dom/manipulation';
export * from 'support/dom/offset';
export * from 'support/dom/traversal';
export interface XY<T = number> {
x: T;
y: T;
}
export interface WH<T = number> {
w: T;
h: T;
}
@@ -1,5 +1,9 @@
import { getBoundingClientRect } from 'support/dom/dimensions';
import { XY } from 'support/dom';
export interface XY<T = number> {
x: T;
y: T;
}
const zeroObj: XY = {
x: 0,
@@ -64,3 +64,11 @@ export const from = <T = any>(arr: ArrayLike<T>) => {
});
return result;
};
/**
* Calls all functions in the passed array of functions.
* @param arr The array filled with function which shall be called.
*/
export const runEach = (arr: Array<((...args: any) => any | any[]) | null | undefined>): void => {
each(arr, (fn) => fn && fn());
};
@@ -49,7 +49,7 @@ describe('dom dimensions', () => {
describe('hasDimensions', () => {
test('DOM element', () => {
const result = hasDimensions(document.body);
expect(result).toBe(true);
expect(result).toBe(false);
});
test('generated element', () => {
@@ -0,0 +1,141 @@
import { off, preventDefault, stopPropagation, OnOptions } from 'support/dom/events';
const testElm = document.body;
const mockEventListener = (passive?: boolean, add?: (...args: any) => any, remove?: (...args: any) => any) => {
const originalAdd = testElm.addEventListener;
const originalRemove = testElm.removeEventListener;
const originalWindow = window.addEventListener;
if (add) {
testElm.addEventListener = add;
}
if (remove) {
testElm.removeEventListener = remove;
}
if (!passive) {
window.addEventListener = () => {};
}
return () => {
testElm.addEventListener = originalAdd;
testElm.removeEventListener = originalRemove;
window.addEventListener = originalWindow;
};
};
describe('dom events', () => {
describe('on', () => {
let eventsModule: any;
const onOffTest = (passive: boolean, eventNames: string, options?: OnOptions) => {
const once = options?._once;
const expectObjAdd = passive
? {
passive: (options && options._passive) || false,
capture: (options && options._capture) || false,
}
: options?._capture || false;
const expectObjRemove = options?._capture || false;
const onceListeners: (() => void)[] = [];
const mockFnRemove = jest.fn();
const mockFnAdd = jest.fn();
const eventListener = () => {};
const revert = mockEventListener(
passive,
(name: string, listener: any, ...args: any) => {
if (once) {
const onceRemoveMockFn = jest.fn();
const revertOnceMock = mockEventListener(passive, jest.fn(), onceRemoveMockFn);
listener();
expect(onceRemoveMockFn).toHaveBeenCalledWith(name, listener, expectObjRemove);
revertOnceMock();
onceListeners.push(listener);
}
mockFnAdd(name, listener, ...args);
},
mockFnRemove
);
const removeFn = eventsModule.on(testElm, eventNames, eventListener, options);
eventNames.split(' ').forEach((eventName, index) => {
expect(mockFnAdd).toHaveBeenCalledWith(eventName, once ? onceListeners[index] : eventListener, expectObjAdd);
});
removeFn();
eventNames.split(' ').forEach((eventName, index) => {
expect(mockFnRemove).toHaveBeenCalledWith(eventName, once ? onceListeners[index] : eventListener, expectObjRemove);
});
revert();
};
beforeEach(() => {
return import('support/dom/events').then((module) => {
eventsModule = module;
jest.resetModules();
});
});
[true, false].forEach((passiveSupport) => {
describe(`passive event listeners support: ${passiveSupport}`, () => {
['testEventName', 'testEventName testEventName2 testEventName3'].forEach((eventNames) => {
const title = eventNames.split(' ').length === 1 ? 'signle' : 'multiple';
test(title, () => {
onOffTest(passiveSupport, eventNames);
onOffTest(passiveSupport, eventNames, { _capture: true });
onOffTest(passiveSupport, eventNames, { _capture: false });
onOffTest(passiveSupport, eventNames, { _capture: true, _passive: true });
onOffTest(passiveSupport, eventNames, { _capture: false, _passive: false });
onOffTest(passiveSupport, eventNames, { _capture: true, _passive: false });
onOffTest(passiveSupport, eventNames, { _capture: false, _passive: true });
onOffTest(passiveSupport, eventNames, { _once: true });
onOffTest(passiveSupport, eventNames, { _once: false });
});
});
});
});
});
describe('off', () => {
const offTest = (eventNames: string, options?: boolean) => {
const mockFnRemove = jest.fn();
const listener = () => {};
const revert = mockEventListener(false, jest.fn(), mockFnRemove);
off(testElm, eventNames, listener, options);
eventNames.split(' ').forEach((eventName) => {
expect(mockFnRemove).toHaveBeenCalledWith(eventName, listener, options);
});
revert();
};
['testEventName', 'testEventName testEventName2 testEventName3'].forEach((eventNames) => {
const title = eventNames.split(' ').length === 1 ? 'signle' : 'multiple';
test(title, () => {
offTest(eventNames, false);
offTest(eventNames, true);
});
});
});
test('preventDefault', () => {
// @ts-ignore
const fakeEvent: Event = {
preventDefault: jest.fn(),
};
preventDefault(fakeEvent);
expect(fakeEvent.preventDefault).toHaveBeenCalled();
});
test('stopPropagation', () => {
// @ts-ignore
const fakeEvent: Event = {
stopPropagation: jest.fn(),
};
stopPropagation(fakeEvent);
expect(fakeEvent.stopPropagation).toHaveBeenCalled();
});
});
@@ -1,4 +1,4 @@
import { each, from, indexOf } from 'support/utils/array';
import { each, from, indexOf, runEach } from 'support/utils/array';
describe('array utilities', () => {
describe('each', () => {
@@ -196,4 +196,14 @@ describe('array utilities', () => {
const idx = indexOf([1, 2, 3], 2);
expect(idx).toBe(1);
});
test('runEach', () => {
const arr = [jest.fn(), null, jest.fn(), undefined, jest.fn()];
runEach(arr);
arr.forEach((fn) => {
if (fn) {
expect(fn).toHaveBeenCalled();
}
});
});
});