mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-07 04:12:27 +03:00
Create DOMObserver
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { rAF, cAF, isEmptyArray, indexOf, createCache, runEach } from 'support';
|
||||
import { getEnvironment } from 'environment';
|
||||
|
||||
export interface AutoUpdateLoop {
|
||||
_add(fn: (delta: number) => any): () => void;
|
||||
_interval(newInterval: number): () => void;
|
||||
_interval(): number;
|
||||
}
|
||||
|
||||
const defaultLoopInterval = 33;
|
||||
let autoUpdateLoopInstance: AutoUpdateLoop;
|
||||
|
||||
const createAutoUpdateLoop = (): AutoUpdateLoop => {
|
||||
let loopIsRunning = false;
|
||||
let loopInterval = defaultLoopInterval;
|
||||
let loopId: number | undefined;
|
||||
const intervals: number[] = [];
|
||||
const loopFunctions: Array<(...args: any) => any> = [];
|
||||
const updateLoopInterval = () => {
|
||||
loopInterval = isEmptyArray(intervals) ? defaultLoopInterval : Math.min.apply(null, intervals);
|
||||
};
|
||||
const updateTimeCache = createCache<number, number>((ctx) => ctx || performance.now(), {
|
||||
_initialValue: performance.now(),
|
||||
_equal: (currTime, newTime) => {
|
||||
const delta = newTime! - currTime!;
|
||||
return delta < loopInterval;
|
||||
},
|
||||
});
|
||||
const loop = (newTime?: number) => {
|
||||
/* istanbul ignore next */
|
||||
if (!isEmptyArray(loopFunctions) && loopIsRunning) {
|
||||
loopId = rAF!(loop);
|
||||
const { _changed, _value, _previous } = updateTimeCache(0, newTime);
|
||||
if (_changed) {
|
||||
runEach(loopFunctions, _value! - _previous!);
|
||||
}
|
||||
}
|
||||
};
|
||||
function interval(): number;
|
||||
function interval(newInterval: number): () => void;
|
||||
function interval(newInterval?: number): number | (() => void) {
|
||||
if (newInterval) {
|
||||
intervals.push(newInterval);
|
||||
updateLoopInterval();
|
||||
|
||||
return () => {
|
||||
intervals.splice(indexOf(intervals, newInterval), 1);
|
||||
updateLoopInterval();
|
||||
};
|
||||
}
|
||||
return loopInterval;
|
||||
}
|
||||
|
||||
return {
|
||||
_add: (fn) => {
|
||||
loopFunctions.push(fn);
|
||||
|
||||
if (!loopIsRunning && !isEmptyArray(loopFunctions)) {
|
||||
getEnvironment()._autoUpdateLoop = loopIsRunning = true;
|
||||
|
||||
updateTimeCache(true);
|
||||
loop();
|
||||
}
|
||||
|
||||
return () => {
|
||||
loopFunctions.splice(indexOf(loopFunctions, fn), 1);
|
||||
|
||||
if (isEmptyArray(loopFunctions) && loopIsRunning) {
|
||||
getEnvironment()._autoUpdateLoop = loopIsRunning = false;
|
||||
|
||||
cAF!(loopId!);
|
||||
loopId = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
_interval: interval,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAutoUpdateLoop = (): AutoUpdateLoop => {
|
||||
if (!autoUpdateLoopInstance) {
|
||||
autoUpdateLoopInstance = createAutoUpdateLoop();
|
||||
}
|
||||
return autoUpdateLoopInstance;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from 'autoUpdateLoop/autoUpdateLoop';
|
||||
@@ -0,0 +1,18 @@
|
||||
export const classNameEnvironment = 'os-environment';
|
||||
export const classNameEnvironmentFlexboxGlue = `${classNameEnvironment}-flexbox-glue`;
|
||||
export const classNameEnvironmentFlexboxGlueMax = `${classNameEnvironmentFlexboxGlue}-max`;
|
||||
|
||||
export const classNameHost = 'os-host';
|
||||
export const classNamePadding = 'os-padding';
|
||||
export const classNameViewport = 'os-viewport';
|
||||
export const classNameContent = 'os-content';
|
||||
export const classNameViewportScrollbarStyling = `${classNameViewport}-scrollbar-styled`;
|
||||
|
||||
export const classNameSizeObserver = 'os-size-observer';
|
||||
export const classNameSizeObserverAppear = `${classNameSizeObserver}-appear`;
|
||||
export const classNameSizeObserverListener = `${classNameSizeObserver}-listener`;
|
||||
export const classNameSizeObserverListenerScroll = `${classNameSizeObserverListener}-scroll`;
|
||||
export const classNameSizeObserverListenerItem = `${classNameSizeObserverListener}-item`;
|
||||
export const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`;
|
||||
|
||||
export const classNameTrinsicObserver = 'os-trinsic-observer';
|
||||
@@ -14,6 +14,12 @@ import {
|
||||
runEach,
|
||||
equalWH,
|
||||
} from 'support';
|
||||
import {
|
||||
classNameEnvironment,
|
||||
classNameEnvironmentFlexboxGlue,
|
||||
classNameEnvironmentFlexboxGlueMax,
|
||||
classNameViewportScrollbarStyling,
|
||||
} from 'classnames';
|
||||
|
||||
export type OnEnvironmentChanged = (env: Environment) => void;
|
||||
export interface Environment {
|
||||
@@ -29,9 +35,6 @@ export interface Environment {
|
||||
|
||||
let environmentInstance: Environment;
|
||||
const { abs, round } = Math;
|
||||
const environmentElmId = 'os-environment';
|
||||
const classNameFlexboxGlue = 'flexbox-glue';
|
||||
const classNameFlexboxGlueMax = `${classNameFlexboxGlue}-max`;
|
||||
|
||||
const getNativeScrollbarSize = (body: HTMLElement, measureElm: HTMLElement): XY => {
|
||||
appendChildren(body, measureElm);
|
||||
@@ -46,7 +49,7 @@ const getNativeScrollbarSize = (body: HTMLElement, measureElm: HTMLElement): XY
|
||||
|
||||
const getNativeScrollbarStyling = (testElm: HTMLElement): boolean => {
|
||||
let result = false;
|
||||
addClass(testElm, 'os-viewport-scrollbar-styled');
|
||||
addClass(testElm, classNameViewportScrollbarStyling);
|
||||
try {
|
||||
result =
|
||||
style(testElm, 'scrollbar-width') === 'none' || window.getComputedStyle(testElm, '::-webkit-scrollbar').getPropertyValue('display') === 'none';
|
||||
@@ -83,12 +86,12 @@ const getRtlScrollBehavior = (parentElm: HTMLElement, childElm: HTMLElement): {
|
||||
};
|
||||
|
||||
const getFlexboxGlue = (parentElm: HTMLElement, childElm: HTMLElement): boolean => {
|
||||
addClass(parentElm, classNameFlexboxGlue);
|
||||
addClass(parentElm, classNameEnvironmentFlexboxGlue);
|
||||
const minOffsetsizeParent = offsetSize(parentElm);
|
||||
const minOffsetsize = offsetSize(childElm);
|
||||
const supportsMin = equalWH(minOffsetsize, minOffsetsizeParent);
|
||||
|
||||
addClass(parentElm, classNameFlexboxGlueMax);
|
||||
addClass(parentElm, classNameEnvironmentFlexboxGlueMax);
|
||||
const maxOffsetsizeParent = offsetSize(parentElm);
|
||||
const maxOffsetsize = offsetSize(childElm);
|
||||
const supportsMax = equalWH(maxOffsetsize, maxOffsetsizeParent);
|
||||
@@ -114,7 +117,7 @@ const diffBiggerThanOne = (valOne: number, valTwo: number): boolean => {
|
||||
|
||||
const createEnvironment = (): Environment => {
|
||||
const { body } = document;
|
||||
const envDOM = createDOM(`<div id="${environmentElmId}"><div></div></div>`);
|
||||
const envDOM = createDOM(`<div class="${classNameEnvironment}"><div></div></div>`);
|
||||
const envElm = envDOM[0] as HTMLElement;
|
||||
const envChildElm = envElm.firstChild as HTMLElement;
|
||||
|
||||
|
||||
@@ -5,11 +5,16 @@ import {
|
||||
createCache,
|
||||
topRightBottomLeft,
|
||||
TRBL,
|
||||
WH,
|
||||
XY,
|
||||
equalTRBL,
|
||||
equalXY,
|
||||
optionsTemplateTypes as oTypes,
|
||||
OptionsTemplateValue,
|
||||
style,
|
||||
OptionsWithOptionsTemplate,
|
||||
scrollSize,
|
||||
offsetSize,
|
||||
} from 'support';
|
||||
import { OSTargetObject } from 'typings';
|
||||
import { createLifecycleBase, Lifecycle } from 'lifecycles/lifecycleBase';
|
||||
@@ -33,11 +38,6 @@ const defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<StructureL
|
||||
},
|
||||
};
|
||||
|
||||
const classNameHost = 'os-host';
|
||||
const classNameViewport = 'os-viewport';
|
||||
const classNameContent = 'os-content';
|
||||
const classNameViewportScrollbarStyling = `${classNameViewport}-scrollbar-styled`;
|
||||
|
||||
const cssMarginEnd = cssProperty('margin-inline-end');
|
||||
const cssBorderEnd = cssProperty('border-inline-end');
|
||||
|
||||
@@ -55,6 +55,13 @@ export const createStructureLifecycle = (
|
||||
const directionObserverObsolete = (cssMarginEnd && cssBorderEnd) || supportsScrollbarStyling || scrollbarsOverlaid.y;
|
||||
|
||||
const updatePaddingCache = createCache(() => topRightBottomLeft(host, 'padding'), { _equal: equalTRBL });
|
||||
const updateOverflowAmountCache = createCache<XY<number>, { _contentScrollSize: WH<number>; _viewportSize: WH<number> }>(
|
||||
(ctx) => ({
|
||||
x: Math.max(0, Math.round((ctx!._contentScrollSize.w - ctx!._viewportSize.w) * 100) / 100),
|
||||
y: Math.max(0, Math.round((ctx!._contentScrollSize.h - ctx!._viewportSize.h) * 100) / 100),
|
||||
}),
|
||||
{ _equal: equalXY }
|
||||
);
|
||||
|
||||
const { _options, _update } = createLifecycleBase<StructureLifecycleOptions>(defaultOptionsWithTemplate, initialOptions, (force, checkOption) => {
|
||||
const { _value: paddingAbsolute, _changed: paddingAbsoluteChanged } = checkOption('paddingAbsolute');
|
||||
@@ -75,11 +82,6 @@ export const createStructureLifecycle = (
|
||||
paddingStyle.l = -padding!.l;
|
||||
}
|
||||
|
||||
if (!supportsScrollbarStyling) {
|
||||
paddingStyle.r -= env._nativeScrollbarSize.y;
|
||||
paddingStyle.b -= env._nativeScrollbarSize.x;
|
||||
}
|
||||
|
||||
style(paddingElm, {
|
||||
top: paddingStyle.t,
|
||||
left: paddingStyle.l,
|
||||
@@ -88,6 +90,59 @@ export const createStructureLifecycle = (
|
||||
'max-width': `calc(100% + ${paddingStyle.r * -1}px)`,
|
||||
});
|
||||
}
|
||||
|
||||
const viewportOffsetSize = offsetSize(paddingElm);
|
||||
const contentClientSize = offsetSize(content);
|
||||
const contentScrollSize = scrollSize(content);
|
||||
const overflowAmuntCache = updateOverflowAmountCache(force, {
|
||||
_contentScrollSize: contentScrollSize,
|
||||
_viewportSize: {
|
||||
w: viewportOffsetSize.w + Math.max(0, contentClientSize.w - contentScrollSize.w),
|
||||
h: viewportOffsetSize.h + Math.max(0, contentClientSize.h - contentScrollSize.h),
|
||||
},
|
||||
});
|
||||
const { _value: overflowAmount, _changed: overflowAmountChanged } = overflowAmuntCache;
|
||||
|
||||
console.log('overflowAmount', overflowAmount);
|
||||
console.log('overflowAmountChanged', overflowAmountChanged);
|
||||
|
||||
/*
|
||||
var setOverflowVariables = function (horizontal) {
|
||||
var scrollbarVars = getScrollbarVars(horizontal);
|
||||
var scrollbarVarsInverted = getScrollbarVars(!horizontal);
|
||||
var xyI = scrollbarVarsInverted._x_y;
|
||||
var xy = scrollbarVars._x_y;
|
||||
var wh = scrollbarVars._w_h;
|
||||
var widthHeight = scrollbarVars._width_height;
|
||||
var scrollMax = _strScroll + scrollbarVars._Left_Top + 'Max';
|
||||
var fractionalOverflowAmount = viewportRect[widthHeight] ? MATH.abs(viewportRect[widthHeight] - _viewportSize[wh]) : 0;
|
||||
var checkFractionalOverflowAmount = previousOverflowAmount && previousOverflowAmount[xy] > 0 && _viewportElementNative[scrollMax] === 0;
|
||||
overflowBehaviorIsVS[xy] = overflowBehavior[xy] === 'v-s';
|
||||
overflowBehaviorIsVH[xy] = overflowBehavior[xy] === 'v-h';
|
||||
overflowBehaviorIsS[xy] = overflowBehavior[xy] === 's';
|
||||
overflowAmount[xy] = MATH.max(0, MATH.round((contentScrollSize[wh] - _viewportSize[wh]) * 100) / 100);
|
||||
overflowAmount[xy] *=
|
||||
hideOverflowForceTextarea || (checkFractionalOverflowAmount && fractionalOverflowAmount > 0 && fractionalOverflowAmount < 1) ? 0 : 1;
|
||||
hasOverflow[xy] = overflowAmount[xy] > 0;
|
||||
|
||||
//hideOverflow:
|
||||
//x || y : true === overflow is hidden by "overflow: scroll" OR "overflow: hidden"
|
||||
//xs || ys : true === overflow is hidden by "overflow: scroll"
|
||||
hideOverflow[xy] =
|
||||
overflowBehaviorIsVS[xy] || overflowBehaviorIsVH[xy]
|
||||
? hasOverflow[xyI] && !overflowBehaviorIsVS[xyI] && !overflowBehaviorIsVH[xyI]
|
||||
: hasOverflow[xy];
|
||||
hideOverflow[xy + 's'] = hideOverflow[xy] ? overflowBehaviorIsS[xy] || overflowBehaviorIsVS[xy] : false;
|
||||
|
||||
canScroll[xy] = hasOverflow[xy] && hideOverflow[xy + 's'];
|
||||
};
|
||||
*/
|
||||
/*
|
||||
if (!supportsScrollbarStyling) {
|
||||
paddingStyle.r -= env._nativeScrollbarSize.y;
|
||||
paddingStyle.b -= env._nativeScrollbarSize.x;
|
||||
}
|
||||
*/
|
||||
});
|
||||
|
||||
const onSizeChanged = () => {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { each, indexOf, isString, MutationObserverConstructor, isEmptyArray, liesBetween } from 'support';
|
||||
import { classNameHost, classNameContent } from 'classnames';
|
||||
|
||||
export interface DOMObserverOptions {
|
||||
_observeContent?: boolean;
|
||||
_attributes?: string[];
|
||||
}
|
||||
export interface DOMObserver {
|
||||
_disconnect: () => void;
|
||||
_update: () => void;
|
||||
}
|
||||
|
||||
const styleChangingAttributes = ['id', 'class', 'style', 'open'];
|
||||
const mutationObserverAttrsTextarea = ['wrap', 'cols', 'rows'];
|
||||
|
||||
const isUnknownMutation = (
|
||||
attributeName: string | null,
|
||||
type: MutationRecordType,
|
||||
observeContent?: boolean,
|
||||
target?: Node,
|
||||
mutationTarget?: Node
|
||||
) => {
|
||||
const isAttributesType = type === 'attributes';
|
||||
const targetIsMutationTarget = target === mutationTarget;
|
||||
const styleChangingAttrChanged = indexOf(styleChangingAttributes, attributeName) > -1;
|
||||
const contentChanged = observeContent && !isAttributesType;
|
||||
const contentAttrChanged =
|
||||
observeContent &&
|
||||
isAttributesType &&
|
||||
styleChangingAttrChanged &&
|
||||
!targetIsMutationTarget &&
|
||||
!liesBetween(mutationTarget as Element | undefined, `.${classNameHost}`, `.${classNameContent}`);
|
||||
const targetAttrChanged = isAttributesType && styleChangingAttrChanged && targetIsMutationTarget && !observeContent;
|
||||
|
||||
return contentChanged || contentAttrChanged || targetAttrChanged;
|
||||
};
|
||||
|
||||
export const createDOMObserver = (
|
||||
target: HTMLElement,
|
||||
callback: (changedTargetAttrs: string[], styleChanged: boolean, contentChanged: boolean) => any,
|
||||
options?: DOMObserverOptions
|
||||
): DOMObserver => {
|
||||
const { _observeContent, _attributes } = options || {};
|
||||
|
||||
// MutationObserver
|
||||
const observedAttributes = (_attributes || []).concat(_observeContent ? styleChangingAttributes : mutationObserverAttrsTextarea);
|
||||
const observerCallback = (mutations: MutationRecord[]) => {
|
||||
let styleChanged = false;
|
||||
let contentChanged = false;
|
||||
const changedTargetAttrs: string[] = [];
|
||||
each(mutations, (mutation) => {
|
||||
const { attributeName, target: mutationTarget, type } = mutation;
|
||||
|
||||
styleChanged = styleChanged || isUnknownMutation(attributeName, type);
|
||||
|
||||
if (_observeContent) {
|
||||
contentChanged = contentChanged || isUnknownMutation(attributeName, type, true, target, mutationTarget);
|
||||
}
|
||||
if (isString(attributeName) && target === mutationTarget) {
|
||||
changedTargetAttrs.push(attributeName);
|
||||
}
|
||||
});
|
||||
|
||||
if (!isEmptyArray(changedTargetAttrs) || styleChanged || contentChanged) {
|
||||
callback(changedTargetAttrs, styleChanged, contentChanged);
|
||||
}
|
||||
};
|
||||
const mutationObserver: MutationObserver = new MutationObserverConstructor!(observerCallback);
|
||||
|
||||
const connect = () => {
|
||||
mutationObserver.observe(target, {
|
||||
attributes: true,
|
||||
attributeOldValue: true,
|
||||
subtree: _observeContent,
|
||||
childList: _observeContent,
|
||||
characterData: _observeContent,
|
||||
attributeFilter: observedAttributes,
|
||||
});
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
return {
|
||||
_disconnect: mutationObserver.disconnect,
|
||||
_update: () => {
|
||||
observerCallback(mutationObserver.takeRecords());
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -17,22 +17,24 @@ import {
|
||||
addClass,
|
||||
isString,
|
||||
equalWH,
|
||||
cAF,
|
||||
rAF,
|
||||
} from 'support';
|
||||
import { CSSDirection } from 'typings';
|
||||
import { getEnvironment } from 'environment';
|
||||
import {
|
||||
classNameSizeObserver,
|
||||
classNameSizeObserverAppear,
|
||||
classNameSizeObserverListener,
|
||||
classNameSizeObserverListenerScroll,
|
||||
classNameSizeObserverListenerItem,
|
||||
classNameSizeObserverListenerItemFinal,
|
||||
} from 'classnames';
|
||||
|
||||
const animationStartEventName = 'animationstart';
|
||||
const scrollEventName = 'scroll';
|
||||
const scrollAmount = 3333333;
|
||||
const ResizeObserverConstructor = jsAPI('ResizeObserver');
|
||||
const classNameSizeObserver = 'os-size-observer';
|
||||
const classNameSizeObserverAppear = `${classNameSizeObserver}-appear`;
|
||||
const classNameSizeObserverListener = `${classNameSizeObserver}-listener`;
|
||||
const classNameSizeObserverListenerScroll = `${classNameSizeObserverListener}-scroll`;
|
||||
const classNameSizeObserverListenerItem = `${classNameSizeObserverListener}-item`;
|
||||
const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`;
|
||||
const cAF = cancelAnimationFrame;
|
||||
const rAF = requestAnimationFrame;
|
||||
const getDirection = (elm: HTMLElement): CSSDirection => style(elm, 'direction') as CSSDirection;
|
||||
|
||||
export type SizeObserverOptions = { _direction?: boolean; _appear?: boolean };
|
||||
@@ -95,8 +97,8 @@ export const createSizeObserver = (
|
||||
isDirty = !scrollEvent || !equalWH(currSize, cacheSize);
|
||||
|
||||
if (scrollEvent && isDirty && !rAFId) {
|
||||
cAF(rAFId);
|
||||
rAFId = rAF(onResized);
|
||||
cAF!(rAFId);
|
||||
rAFId = rAF!(onResized);
|
||||
} else if (!scrollEvent) {
|
||||
onResized();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { WH, Cache, createDOM, offsetSize, jsAPI, runEach, prependChildren, removeElements, createCache } from 'support';
|
||||
import { WH, Cache, createDOM, offsetSize, runEach, prependChildren, removeElements, createCache, IntersectionObserverConstructor } from 'support';
|
||||
import { createSizeObserver } from 'observers/sizeObserver';
|
||||
|
||||
const classNameTrinsicObserver = 'os-trinsic-observer';
|
||||
const IntersectionObserverConstructor = jsAPI('IntersectionObserver');
|
||||
import { classNameTrinsicObserver } from 'classnames';
|
||||
|
||||
export const createTrinsicObserver = (
|
||||
target: HTMLElement,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import './trinsicobserver.scss';
|
||||
@import './structurelifecycle.scss';
|
||||
|
||||
#os-environment {
|
||||
.os-environment {
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
@@ -16,7 +16,7 @@
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
&.flexbox-glue {
|
||||
&.os-environment-flexbox-glue {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.flexbox-glue-max {
|
||||
&.os-environment-flexbox-glue-max {
|
||||
max-height: 200px;
|
||||
|
||||
div {
|
||||
|
||||
@@ -4,11 +4,7 @@ import { Cache, appendChildren, addClass, contents, is, isHTMLElement, createDiv
|
||||
import { createSizeObserver } from 'observers/sizeObserver';
|
||||
import { createTrinsicObserver } from 'observers/trinsicObserver';
|
||||
import { Lifecycle } from 'lifecycles/lifecycleBase';
|
||||
|
||||
const classNameHost = 'os-host';
|
||||
const classNamePadding = 'os-padding';
|
||||
const classNameViewport = 'os-viewport';
|
||||
const classNameContent = 'os-content';
|
||||
import { classNameHost, classNamePadding, classNameViewport, classNameContent } from 'classnames';
|
||||
|
||||
const normalizeTarget = (target: OSTarget): OSTargetObject => {
|
||||
if (isHTMLElement(target)) {
|
||||
|
||||
+4
-4
@@ -13,20 +13,20 @@ export type CacheUpdate<T, C> = (force?: boolean | 0, context?: C) => Cache<T>;
|
||||
|
||||
export type UpdateCachePropFunction<T, C> = (context?: C, current?: T, previous?: T) => T;
|
||||
|
||||
export type EqualCachePropFunction<T> = (a?: T, b?: T) => boolean;
|
||||
export type EqualCachePropFunction<T> = (currentVal?: T, newVal?: T) => boolean;
|
||||
|
||||
export const createCache = <T, C = undefined>(update: UpdateCachePropFunction<T, C>, options?: CacheOptions<T>): CacheUpdate<T, C> => {
|
||||
const { _equal, _initialValue } = options || {};
|
||||
let _value: T | undefined = _initialValue;
|
||||
let _previous: T | undefined;
|
||||
return (force, context) => {
|
||||
const prev = _value;
|
||||
const curr = _value;
|
||||
const newVal = update(context, _value, _previous);
|
||||
const changed = force || (_equal ? !_equal(prev, newVal) : prev !== newVal);
|
||||
const changed = force || (_equal ? !_equal(curr, newVal) : curr !== newVal);
|
||||
|
||||
if (changed) {
|
||||
_value = newVal;
|
||||
_previous = prev;
|
||||
_previous = curr;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { jsAPI } from 'support/compatibility/vendors';
|
||||
|
||||
export const resizeObserver: any | undefined = jsAPI('ResizeObserver');
|
||||
export const MutationObserverConstructor = jsAPI<typeof MutationObserver>('MutationObserver');
|
||||
export const IntersectionObserverConstructor = jsAPI<typeof IntersectionObserver>('IntersectionObserver');
|
||||
export const ResizeObserverConstructor: any | undefined = jsAPI('ResizeObserver');
|
||||
export const cAF = jsAPI<typeof cancelAnimationFrame>('cancelAnimationFrame');
|
||||
export const rAF = jsAPI<typeof requestAnimationFrame>('requestAnimationFrame');
|
||||
|
||||
@@ -18,8 +18,8 @@ export const windowSize = (): WH => ({
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the offset- width and height of the passed element. If the element is null the width and height values are 0.
|
||||
* @param elm The element of which the offset- width and height shall be returned.
|
||||
* Returns the scroll- width and height of the passed element. If the element is null the width and height values are 0.
|
||||
* @param elm The element of which the scroll- width and height shall be returned.
|
||||
*/
|
||||
export const offsetSize = (elm: HTMLElement | null): WH =>
|
||||
elm
|
||||
@@ -41,6 +41,18 @@ export const clientSize = (elm: HTMLElement | null): WH =>
|
||||
}
|
||||
: zeroObj;
|
||||
|
||||
/**
|
||||
* Returns the client- width and height of the passed element. If the element is null the width and height values are 0.
|
||||
* @param elm The element of which the client- width and height shall be returned.
|
||||
*/
|
||||
export const scrollSize = (elm: HTMLElement | null): WH =>
|
||||
elm
|
||||
? {
|
||||
w: elm.scrollWidth,
|
||||
h: elm.scrollHeight,
|
||||
}
|
||||
: zeroObj;
|
||||
|
||||
/**
|
||||
* Returns the BoundingClientRect of the passed element.
|
||||
* @param elm The element of which the BoundingClientRect shall be returned.
|
||||
|
||||
@@ -69,7 +69,7 @@ export const prependChildren = (node: Node | null, children: NodeCollection): vo
|
||||
* @param insertedNodes The Nodes which shall be inserted.
|
||||
*/
|
||||
export const insertBefore = (node: Node | null, insertedNodes: NodeCollection): void => {
|
||||
before(parent(node), node, insertedNodes);
|
||||
before(parent(node as Element), node, insertedNodes);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,7 +78,7 @@ export const insertBefore = (node: Node | null, insertedNodes: NodeCollection):
|
||||
* @param insertedNodes The Nodes which shall be inserted.
|
||||
*/
|
||||
export const insertAfter = (node: Node | null, insertedNodes: NodeCollection): void => {
|
||||
before(parent(node), node && node.nextSibling, insertedNodes);
|
||||
before(parent(node as Element), node && node.nextSibling, insertedNodes);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -89,7 +89,7 @@ export const removeElements = (nodes: NodeCollection): void => {
|
||||
if (isArrayLike(nodes)) {
|
||||
each(from(nodes), (e) => removeElements(e));
|
||||
} else if (nodes) {
|
||||
const parentElm = parent(nodes);
|
||||
const parentElm = parent(nodes as Element);
|
||||
if (parentElm) {
|
||||
parentElm.removeChild(nodes);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { each, from } from 'support/utils/array';
|
||||
|
||||
const matches = (elm: Element | null, selector: string): boolean => {
|
||||
if (elm) {
|
||||
/* istanbul ignore next */
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
const fn = Element.prototype.matches || Element.prototype.msMatchesSelector;
|
||||
return fn.call(elm, selector);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
type InputElementType = Element | null | undefined;
|
||||
type OutputElementType = Element | null;
|
||||
|
||||
const elmPrototype = Element.prototype;
|
||||
|
||||
/**
|
||||
* Find all elements with the passed selector, outgoing (and including) the passed element or the document if no element was provided.
|
||||
* @param selector The selector which has to be searched by.
|
||||
* @param elm The element from which the search shall be outgoing.
|
||||
*/
|
||||
export const find = (selector: string, elm?: Element | null): ReadonlyArray<Element> => {
|
||||
const find = (selector: string, elm?: InputElementType): ReadonlyArray<Element> => {
|
||||
const arr: Array<Element> = [];
|
||||
|
||||
each((elm || document).querySelectorAll(selector), (e: Element) => {
|
||||
@@ -31,26 +25,35 @@ export const find = (selector: string, elm?: Element | null): ReadonlyArray<Elem
|
||||
* @param selector The selector which has to be searched by.
|
||||
* @param elm The element from which the search shall be outgoing.
|
||||
*/
|
||||
export const findFirst = (selector: string, elm?: Element | null): Element | null => (elm || document).querySelector(selector);
|
||||
const findFirst = (selector: string, elm?: InputElementType): OutputElementType => (elm || document).querySelector(selector);
|
||||
|
||||
/**
|
||||
* Determines whether the passed element is matching with the passed selector.
|
||||
* @param elm The element which has to be compared with the passed selector.
|
||||
* @param selector The selector which has to be compared with the passed element. Additional selectors: ':visible' and ':hidden'.
|
||||
*/
|
||||
export const is = (elm: Element | null, selector: string): boolean => matches(elm, selector);
|
||||
const is = (elm: InputElementType, selector: string): boolean => {
|
||||
if (elm) {
|
||||
/* istanbul ignore next */
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
const fn = elmPrototype.matches || elmPrototype.msMatchesSelector;
|
||||
return fn.call(elm, selector);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the children (no text-nodes or comments) of the passed element which are matching the passed selector. An empty array is returned if the passed element is null.
|
||||
* @param elm The element of which the children shall be returned.
|
||||
* @param selector The selector which must match with the children elements.
|
||||
*/
|
||||
export const children = (elm: Element | null, selector?: string): ReadonlyArray<Element> => {
|
||||
const children = (elm: InputElementType, selector?: string): ReadonlyArray<Element> => {
|
||||
const childs: Array<Element> = [];
|
||||
|
||||
each(elm && elm.children, (child: Element) => {
|
||||
if (selector) {
|
||||
if (matches(child, selector)) {
|
||||
if (is(child, selector)) {
|
||||
childs.push(child);
|
||||
}
|
||||
} else {
|
||||
@@ -65,10 +68,47 @@ export const children = (elm: Element | null, selector?: string): ReadonlyArray<
|
||||
* Returns the childNodes (incl. text-nodes or comments etc.) of the passed element. An empty array is returned if the passed element is null.
|
||||
* @param elm The element of which the childNodes shall be returned.
|
||||
*/
|
||||
export const contents = (elm: Element | null): ReadonlyArray<ChildNode> => (elm ? from(elm.childNodes) : []);
|
||||
const contents = (elm: InputElementType): ReadonlyArray<ChildNode> => (elm ? from(elm.childNodes) : []);
|
||||
|
||||
/**
|
||||
* Returns the parent element of the passed element, or null if the passed element is null.
|
||||
* @param elm The element of which the parent element shall be returned.
|
||||
*/
|
||||
export const parent = (elm: Node | null): Node | null => (elm ? elm.parentElement : null);
|
||||
const parent = (elm: InputElementType): OutputElementType => (elm ? elm.parentElement : null);
|
||||
|
||||
const closest = (elm: InputElementType, selector: string): OutputElementType => {
|
||||
if (elm) {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
if (elmPrototype.closest) {
|
||||
return elm.closest(selector);
|
||||
}
|
||||
do {
|
||||
if (is(elm, selector)) {
|
||||
return elm;
|
||||
}
|
||||
elm = parent(elm);
|
||||
} while (elm !== null && elm.nodeType === 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether the given element lies between two selectors in the DOM.
|
||||
* @param elm The element.
|
||||
* @param highBoundarySelector The high boundary selector.
|
||||
* @param deepBoundarySelector The deep boundary selector.
|
||||
*/
|
||||
const liesBetween = (elm: InputElementType, highBoundarySelector: string, deepBoundarySelector: string): boolean => {
|
||||
const closestHighBoundaryElm = closest(elm, highBoundarySelector);
|
||||
const closestDeepBoundaryElm = findFirst(deepBoundarySelector, closestHighBoundaryElm);
|
||||
|
||||
return closestHighBoundaryElm && closestDeepBoundaryElm
|
||||
? closestHighBoundaryElm === elm ||
|
||||
closestDeepBoundaryElm === elm ||
|
||||
closest(closest(elm, deepBoundarySelector), highBoundarySelector) !== closestHighBoundaryElm
|
||||
: false;
|
||||
};
|
||||
|
||||
export { find, findFirst, is, children, contents, parent, liesBetween };
|
||||
|
||||
@@ -15,23 +15,26 @@ export function each<T>(
|
||||
callback: (value: T, indexOrKey: number, source: Array<T>) => boolean | void
|
||||
): Array<T> | ReadonlyArray<T>;
|
||||
export function each<T>(
|
||||
array: Array<T> | ReadonlyArray<T> | null,
|
||||
array: Array<T> | ReadonlyArray<T> | null | undefined,
|
||||
callback: (value: T, indexOrKey: number, source: Array<T>) => boolean | void
|
||||
): Array<T> | ReadonlyArray<T> | null;
|
||||
): Array<T> | ReadonlyArray<T> | null | undefined;
|
||||
export function each<T>(
|
||||
arrayLikeObject: ArrayLike<T>,
|
||||
callback: (value: T, indexOrKey: number, source: ArrayLike<T>) => boolean | void
|
||||
): ArrayLike<T>;
|
||||
export function each<T>(
|
||||
arrayLikeObject: ArrayLike<T> | null,
|
||||
arrayLikeObject: ArrayLike<T> | null | undefined,
|
||||
callback: (value: T, indexOrKey: number, source: ArrayLike<T>) => boolean | void
|
||||
): ArrayLike<T> | null;
|
||||
): ArrayLike<T> | null | undefined;
|
||||
export function each(obj: PlainObject, callback: (value: any, indexOrKey: string, source: PlainObject) => boolean | void): PlainObject;
|
||||
export function each(obj: PlainObject | null, callback: (value: any, indexOrKey: string, source: PlainObject) => boolean | void): PlainObject | null;
|
||||
export function each(
|
||||
obj: PlainObject | null | undefined,
|
||||
callback: (value: any, indexOrKey: string, source: PlainObject) => boolean | void
|
||||
): PlainObject | null | undefined;
|
||||
export function each<T>(
|
||||
source: ArrayLike<T> | PlainObject | null,
|
||||
source: ArrayLike<T> | PlainObject | null | undefined,
|
||||
callback: (value: T, indexOrKey: any, source: any) => boolean | void
|
||||
): Array<T> | ReadonlyArray<T> | ArrayLike<T> | PlainObject | null {
|
||||
): Array<T> | ReadonlyArray<T> | ArrayLike<T> | PlainObject | null | undefined {
|
||||
if (isArrayLike(source)) {
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
if (callback(source[i], i, source) === false) {
|
||||
@@ -67,14 +70,21 @@ export const from = <T = any>(arr: ArrayLike<T>) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether the passed array is empty.
|
||||
* @param array The array which shall be checked.
|
||||
*/
|
||||
export const isEmptyArray = (array: Array<any> | null | undefined) => array && array.length === 0;
|
||||
|
||||
/**
|
||||
* Calls all functions in the passed array/set of functions.
|
||||
* @param arr The array filled with function which shall be called.
|
||||
* @param p1 The first param.
|
||||
*/
|
||||
export const runEach = (arr: ArrayLike<RunEachItem> | Set<RunEachItem>): void => {
|
||||
export const runEach = (arr: ArrayLike<RunEachItem> | Set<RunEachItem>, p1?: unknown): void => {
|
||||
if (arr instanceof Set) {
|
||||
arr.forEach((fn) => fn && fn());
|
||||
arr.forEach((fn) => fn && fn(p1));
|
||||
} else {
|
||||
each(arr, (fn) => fn && fn());
|
||||
each(arr, (fn) => fn && fn(p1));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from 'support/utils/array';
|
||||
export * from 'support/utils/equal';
|
||||
export * from 'support/utils/lexicon';
|
||||
export * from 'support/utils/object';
|
||||
export * from 'support/utils/types';
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
interface GenericLexicon<T extends boolean> {
|
||||
_widthHeight: T extends true ? 'width' : 'height';
|
||||
_WidthHeight: T extends true ? 'Width' : 'Height';
|
||||
_leftTop: T extends true ? 'left' : 'top';
|
||||
_LeftTop: T extends true ? 'Left' : 'Top';
|
||||
_xy: T extends true ? 'x' : 'y';
|
||||
_XY: T extends true ? 'X' : 'Y';
|
||||
_wh: T extends true ? 'w' : 'h';
|
||||
_lt: T extends true ? 'l' : 't';
|
||||
}
|
||||
|
||||
export interface Lexicon<T extends boolean> extends GenericLexicon<T> {
|
||||
_inverted: Lexicon<T extends true ? false : true>;
|
||||
}
|
||||
|
||||
export const getLexicon = <T extends boolean = false>(horizontal?: T): Lexicon<T> => {
|
||||
return {
|
||||
_widthHeight: horizontal ? 'width' : 'height',
|
||||
_WidthHeight: horizontal ? 'Width' : 'Height',
|
||||
_leftTop: horizontal ? 'left' : 'top',
|
||||
_LeftTop: horizontal ? 'Left' : 'Top',
|
||||
_xy: horizontal ? 'x' : 'y',
|
||||
_XY: horizontal ? 'X' : 'Y',
|
||||
_wh: horizontal ? 'w' : 'h',
|
||||
_lt: horizontal ? 'l' : 't',
|
||||
_inverted: getLexicon(!horizontal),
|
||||
} as Lexicon<T>;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { getAutoUpdateLoop } from 'autoUpdateLoop';
|
||||
import { getEnvironment } from 'environment';
|
||||
|
||||
describe('autoUpdateLoop', () => {
|
||||
test('first creation', async () => {
|
||||
const deltas: number[] = [];
|
||||
const wait = 2700;
|
||||
const loop = getAutoUpdateLoop();
|
||||
const defaultInterval = loop._interval();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const added = Date.now();
|
||||
const remove = loop._add((delta) => {
|
||||
if (deltas.length === 0) {
|
||||
expect(Date.now() - added >= defaultInterval).toBe(true);
|
||||
}
|
||||
expect(delta >= defaultInterval).toBe(true);
|
||||
deltas.push(delta);
|
||||
});
|
||||
expect(getEnvironment()._autoUpdateLoop).toBe(true);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, wait));
|
||||
const elapsedDeltas = deltas.reduce((a, b) => a + b, 0);
|
||||
|
||||
expect(wait - elapsedDeltas < defaultInterval * 2).toBe(true);
|
||||
|
||||
remove();
|
||||
expect(getEnvironment()._autoUpdateLoop).toBe(false);
|
||||
});
|
||||
|
||||
test('add multiple', async () => {
|
||||
const loop = getAutoUpdateLoop();
|
||||
const fn1 = jest.fn();
|
||||
const fn2 = jest.fn();
|
||||
const fn3 = jest.fn();
|
||||
|
||||
const remove1 = loop._add(fn1);
|
||||
const remove2 = loop._add(fn2);
|
||||
const remove3 = loop._add(fn3);
|
||||
|
||||
expect(getEnvironment()._autoUpdateLoop).toBe(true);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2500));
|
||||
|
||||
expect(fn1).toHaveBeenCalledTimes(fn1.mock.calls.length);
|
||||
expect(fn2).toHaveBeenCalledTimes(fn1.mock.calls.length);
|
||||
expect(fn3).toHaveBeenCalledTimes(fn1.mock.calls.length);
|
||||
|
||||
remove1();
|
||||
remove2();
|
||||
remove3();
|
||||
|
||||
expect(getEnvironment()._autoUpdateLoop).toBe(false);
|
||||
});
|
||||
|
||||
test('change interval', async () => {
|
||||
const loop = getAutoUpdateLoop();
|
||||
const defaultInterval = loop._interval();
|
||||
|
||||
const remove10 = loop._interval(10);
|
||||
const remove5 = loop._interval(5);
|
||||
const remove3 = loop._interval(3);
|
||||
const remove8 = loop._interval(8);
|
||||
const remove15 = loop._interval(15);
|
||||
|
||||
expect(loop._interval()).toBe(3);
|
||||
remove3();
|
||||
expect(loop._interval()).toBe(5);
|
||||
remove10();
|
||||
remove8();
|
||||
expect(loop._interval()).toBe(5);
|
||||
remove5();
|
||||
expect(loop._interval()).toBe(15);
|
||||
remove15();
|
||||
|
||||
expect(loop._interval()).toBe(defaultInterval);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isNumber, isPlainObject } from 'support/utils/types';
|
||||
import { createDiv } from 'support/dom/create';
|
||||
import { windowSize, offsetSize, clientSize, getBoundingClientRect, hasDimensions } from 'support/dom/dimensions';
|
||||
import { windowSize, offsetSize, clientSize, scrollSize, getBoundingClientRect, hasDimensions } from 'support/dom/dimensions';
|
||||
|
||||
describe('dom dimensions', () => {
|
||||
describe('offsetSize', () => {
|
||||
@@ -35,6 +35,22 @@ describe('dom dimensions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrollSize', () => {
|
||||
test('DOM element', () => {
|
||||
const result = scrollSize(document.body);
|
||||
expect(isPlainObject(result)).toBe(true);
|
||||
expect(isNumber(result.w)).toBe(true);
|
||||
expect(isNumber(result.h)).toBe(true);
|
||||
});
|
||||
|
||||
test('null', () => {
|
||||
const result = scrollSize(null);
|
||||
expect(isPlainObject(result)).toBe(true);
|
||||
expect(isNumber(result.w)).toBe(true);
|
||||
expect(isNumber(result.h)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('windowSize', () => {
|
||||
const result = windowSize();
|
||||
expect(isPlainObject(result)).toBe(true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { find, findFirst, is, children, contents, parent, createDiv } from 'support/dom';
|
||||
import { find, findFirst, is, children, contents, parent, createDiv, liesBetween } from 'support/dom';
|
||||
|
||||
const slotElm = document.body;
|
||||
const testHTML = '<div id="parent" class="div-class"><div id="child" class="div-class"></div></div><p>2</p><input type="text" value="3"></input>abc';
|
||||
@@ -199,4 +199,92 @@ describe('dom traversal', () => {
|
||||
expect(p).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('liesBetween', () => {
|
||||
const elmsBetween = ['.host', '.something', '.something-a', '.something-b', '.padding', '.viewport', '.content'];
|
||||
const elmsOutside = ['.allowed-a', '.allowed-b', '.allowed-c', '.deeper-a', '.deeper-b'];
|
||||
const elmsToTest = [...elmsBetween, ...elmsOutside];
|
||||
const domPart = (id: string, content?: string) => `
|
||||
<div id="${id}" class="host">
|
||||
<div class="something">
|
||||
<div class="something-a">
|
||||
<div class="something-b">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="padding">
|
||||
<div class="viewport">
|
||||
<div class="content">
|
||||
<div class="allowed-a"><div class="allowed-b"><div class="allowed-c"></div></div></div>
|
||||
<div class="deeper-a"><div class="deeper-b">${content || ''}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const createTestDOM = (nestings = 0) => {
|
||||
let part = '';
|
||||
for (let i = 0; i < nestings + 1; i++) {
|
||||
part = domPart(`host-${nestings - i}`, part);
|
||||
}
|
||||
|
||||
return part;
|
||||
};
|
||||
const genericTest = (nestings: number) => {
|
||||
const allHostIds = Array(nestings)
|
||||
.fill(0)
|
||||
.map((_, index) => `#host-${index}`);
|
||||
|
||||
const test = (hostId: string, remainingIds: string[]) => {
|
||||
const runExpectance = (id: string) => {
|
||||
const isRemainingId = remainingIds.includes(id);
|
||||
elmsToTest.forEach((elm) => {
|
||||
const hostElm = findFirst(`${id}`);
|
||||
const searchElm = hostElm?.classList.contains(elm.substring(1)) ? hostElm : findFirst(`${id} ${elm}`);
|
||||
|
||||
expect(liesBetween(searchElm, `${hostId}`, '.content')).toBe(
|
||||
isRemainingId ? false : elmsBetween.includes(elm) ? true : elmsOutside.includes(elm) ? false : undefined
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
runExpectance(hostId);
|
||||
|
||||
remainingIds.forEach((id) => {
|
||||
runExpectance(id);
|
||||
});
|
||||
};
|
||||
|
||||
elmsBetween.forEach((elm) => {
|
||||
expect(liesBetween(findFirst(elm), '.host', '.content')).toBe(true);
|
||||
});
|
||||
|
||||
elmsOutside.forEach((elm) => {
|
||||
expect(liesBetween(findFirst(elm), '.host', '.content')).toBe(false);
|
||||
});
|
||||
|
||||
for (let i = 0; i < nestings; i++) {
|
||||
const currHostId = allHostIds[i];
|
||||
const remainingIds = allHostIds.filter((id) => id !== currHostId);
|
||||
|
||||
test(currHostId, remainingIds);
|
||||
}
|
||||
};
|
||||
|
||||
test('with native closest', () => {
|
||||
slotElm.innerHTML = createTestDOM(3);
|
||||
genericTest(3);
|
||||
});
|
||||
|
||||
test('with polyfill closest', () => {
|
||||
const original = Element.prototype.closest;
|
||||
// @ts-ignore
|
||||
Element.prototype.closest = undefined;
|
||||
|
||||
slotElm.innerHTML = createTestDOM(3);
|
||||
genericTest(3);
|
||||
|
||||
Element.prototype.closest = original;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user