Create DOMObserver

This commit is contained in:
Rene
2021-01-08 21:50:56 +01:00
parent 6810865045
commit 219c218f44
21 changed files with 601 additions and 78 deletions
@@ -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
View File
@@ -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;
});
});
});