mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-19 14:40:35 +03:00
add dom events into support lib
This commit is contained in:
@@ -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';
|
import { Environment } from 'environment';
|
||||||
|
|
||||||
const animationStartEventName = 'animationstart';
|
const animationStartEventName = 'animationstart';
|
||||||
@@ -14,10 +28,8 @@ const rAF = requestAnimationFrame;
|
|||||||
const getDirection = (elm: HTMLElement) => style(elm, 'direction');
|
const getDirection = (elm: HTMLElement) => style(elm, 'direction');
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// 1. handling for event listeners (animationStartEventName.split(' '))
|
// 1. MAYBE add comparison function to offsetSize etc.
|
||||||
// 2. return not just element but also destruction function
|
// 2. remove supportPassiveListeners & resizeobserver from environment
|
||||||
// 3. shorthand handling for preventDefault & stopPropagation etc.
|
|
||||||
// 5. MAYBE add comparison function to offsetSize etc.
|
|
||||||
|
|
||||||
export const createSizeObserver = (
|
export const createSizeObserver = (
|
||||||
target: HTMLElement,
|
target: HTMLElement,
|
||||||
@@ -37,6 +49,7 @@ export const createSizeObserver = (
|
|||||||
}
|
}
|
||||||
onSizeChangedCallback(dir === true);
|
onSizeChangedCallback(dir === true);
|
||||||
};
|
};
|
||||||
|
const offListeners: (() => void)[] = [];
|
||||||
let appearCallback: (...args: any) => any = onSizeChangedCallbackProxy;
|
let appearCallback: (...args: any) => any = onSizeChangedCallbackProxy;
|
||||||
|
|
||||||
if (ResizeObserverConstructor) {
|
if (ResizeObserverConstructor) {
|
||||||
@@ -81,14 +94,14 @@ export const createSizeObserver = (
|
|||||||
|
|
||||||
reset();
|
reset();
|
||||||
if (scrollEvent) {
|
if (scrollEvent) {
|
||||||
scrollEvent.preventDefault();
|
preventDefault(scrollEvent);
|
||||||
scrollEvent.stopPropagation();
|
stopPropagation(scrollEvent);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
expandElement.addEventListener(scrollEventName, onScroll);
|
offListeners.push(on(expandElement, scrollEventName, onScroll));
|
||||||
shrinkElement.addEventListener(scrollEventName, onScroll);
|
offListeners.push(on(shrinkElement, scrollEventName, onScroll));
|
||||||
|
|
||||||
// lets assume that the divs will never be that large and a constant value is enough
|
// lets assume that the divs will never be that large and a constant value is enough
|
||||||
style(expandElementChild, {
|
style(expandElementChild, {
|
||||||
@@ -99,36 +112,34 @@ export const createSizeObserver = (
|
|||||||
appearCallback = onScroll;
|
appearCallback = onScroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
each(animationStartEventName.split(' '), (eventName) => {
|
|
||||||
sizeObserver.addEventListener(eventName, () => {
|
|
||||||
appearCallback();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (direction) {
|
if (direction) {
|
||||||
let dirCache: string | undefined;
|
let dirCache: string | undefined;
|
||||||
sizeObserver.addEventListener('scroll', (event: Event) => {
|
offListeners.push(
|
||||||
const dir = getDirection(sizeObserver);
|
on(sizeObserver, scrollEventName, (event: Event) => {
|
||||||
const changed = dir !== dirCache;
|
const dir = getDirection(sizeObserver);
|
||||||
if (changed) {
|
const changed = dir !== dirCache;
|
||||||
if (dir === 'rtl') {
|
if (changed) {
|
||||||
style(listenerElement, { left: 'auto', right: 0 });
|
if (dir === 'rtl') {
|
||||||
} else {
|
style(listenerElement, { left: 'auto', right: 0 });
|
||||||
style(listenerElement, { left: 0, right: 'auto' });
|
} else {
|
||||||
|
style(listenerElement, { left: 0, right: 'auto' });
|
||||||
|
}
|
||||||
|
dirCache = dir;
|
||||||
|
onSizeChangedCallbackProxy(true);
|
||||||
}
|
}
|
||||||
dirCache = dir;
|
|
||||||
onSizeChangedCallbackProxy(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
preventDefault(event);
|
||||||
event.stopPropagation();
|
stopPropagation(event);
|
||||||
return false;
|
return false;
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offListeners.push(on(sizeObserver, animationStartEventName, appearCallback));
|
||||||
prependChildren(target, sizeObserver);
|
prependChildren(target, sizeObserver);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
runEach(offListeners);
|
||||||
removeElements(sizeObserver);
|
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 elementHasDimensions = (elm: HTMLElement): boolean => !!(elm.offsetWidth || elm.offsetHeight || elm.getClientRects().length);
|
||||||
const zeroObj: WH = {
|
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/class';
|
||||||
export * from 'support/dom/create';
|
export * from 'support/dom/create';
|
||||||
export * from 'support/dom/dimensions';
|
export * from 'support/dom/dimensions';
|
||||||
|
export * from 'support/dom/events';
|
||||||
export * from 'support/dom/style';
|
export * from 'support/dom/style';
|
||||||
export * from 'support/dom/manipulation';
|
export * from 'support/dom/manipulation';
|
||||||
export * from 'support/dom/offset';
|
export * from 'support/dom/offset';
|
||||||
export * from 'support/dom/traversal';
|
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 { getBoundingClientRect } from 'support/dom/dimensions';
|
||||||
import { XY } from 'support/dom';
|
|
||||||
|
export interface XY<T = number> {
|
||||||
|
x: T;
|
||||||
|
y: T;
|
||||||
|
}
|
||||||
|
|
||||||
const zeroObj: XY = {
|
const zeroObj: XY = {
|
||||||
x: 0,
|
x: 0,
|
||||||
|
|||||||
@@ -64,3 +64,11 @@ export const from = <T = any>(arr: ArrayLike<T>) => {
|
|||||||
});
|
});
|
||||||
return result;
|
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', () => {
|
describe('hasDimensions', () => {
|
||||||
test('DOM element', () => {
|
test('DOM element', () => {
|
||||||
const result = hasDimensions(document.body);
|
const result = hasDimensions(document.body);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generated element', () => {
|
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('array utilities', () => {
|
||||||
describe('each', () => {
|
describe('each', () => {
|
||||||
@@ -196,4 +196,14 @@ describe('array utilities', () => {
|
|||||||
const idx = indexOf([1, 2, 3], 2);
|
const idx = indexOf([1, 2, 3], 2);
|
||||||
expect(idx).toBe(1);
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user