mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-05-24 04:54:06 +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';
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user