add debounce and updating options

This commit is contained in:
Rene
2021-05-13 23:06:51 +02:00
parent 99d1c25dae
commit 0a18f93441
9 changed files with 420 additions and 281 deletions
@@ -1,28 +1,11 @@
import {
XY,
WH,
TRBL,
CacheValues,
PartialOptions,
each,
hasOwnProperty,
isNumber,
scrollLeft,
scrollTop,
assignDeep,
liesBetween,
diffClass,
} from 'support';
import { XY, WH, TRBL, CacheValues, PartialOptions, each, hasOwnProperty, isNumber, scrollLeft, scrollTop, assignDeep } from 'support';
import { OSOptions } from 'options';
import { classNameHost, classNameViewport, classNameContent } from 'classnames';
import { getEnvironment } from 'environment';
import { StructureSetup } from 'setups/structureSetup';
import { lifecycleHubOservers } from 'lifecycles/lifecycleHubObservers';
import { createTrinsicLifecycle } from 'lifecycles/trinsicLifecycle';
import { createPaddingLifecycle } from 'lifecycles/paddingLifecycle';
import { createOverflowLifecycle } from 'lifecycles/overflowLifecycle';
import { createSizeObserver } from 'observers/sizeObserver';
import { createTrinsicObserver } from 'observers/trinsicObserver';
import { createDOMObserver } from 'observers/domObserver';
import { StyleObject } from 'typings';
export type LifecycleCheckOption = <T>(path: string) => LifecycleOptionInfo<T>;
@@ -82,28 +65,7 @@ export interface LifecycleHub {
const getPropByPath = <T>(obj: any, path: string): T =>
obj ? path.split('.').reduce((o, prop) => (o && hasOwnProperty(o, prop) ? o[prop] : undefined), obj) : undefined;
// TODO: observer textarea attrs if textarea
// TODO: tabindex, open etc.
// TODO: test _ignoreContentChange & _ignoreNestedTargetChange for content dom observer
// TODO: test _ignoreTargetChange for target dom observer
const ignorePrefix = 'os-';
const hostSelector = `.${classNameHost}`;
const viewportSelector = `.${classNameViewport}`;
const contentSelector = `.${classNameContent}`;
const attrs = ['id', 'class', 'style', 'open'];
const ignoreTargetChange = (target: Node, attrName: string, oldValue: string | null, newValue: string | null) => {
if (attrName === 'class' && oldValue && newValue) {
const diff = diffClass(oldValue, newValue);
return !!diff.find((addedOrRemovedClass) => addedOrRemovedClass.indexOf(ignorePrefix) !== 0);
}
return false;
};
const directionIsRTLCacheValuesFallback: CacheValues<boolean> = {
_value: false,
_previous: false,
_changed: false,
};
const heightIntrinsicCacheValuesFallback: CacheValues<boolean> = {
const booleanCacheValuesFallback: CacheValues<boolean> = {
_value: false,
_previous: false,
_changed: false,
@@ -139,7 +101,7 @@ const lifecycleCommunicationFallback: LifecycleCommunication = {
export const createLifecycleHub = (options: OSOptions, structureSetup: StructureSetup): LifecycleHubInstance => {
let lifecycleCommunication = lifecycleCommunicationFallback;
const { _host, _viewport, _content } = structureSetup._targetObj;
const { _viewport } = structureSetup._targetObj;
const {
_nativeScrollbarStyling,
_nativeScrollbarIsOverlaid,
@@ -168,10 +130,11 @@ export const createLifecycleHub = (options: OSOptions, structureSetup: Structure
_contentMutation = force || false,
_paddingStyleChanged = force || false,
} = updateHints || {};
const finalDirectionIsRTL =
_directionIsRTL || (sizeObserver ? sizeObserver._getCurrentCacheValues(force)._directionIsRTL : directionIsRTLCacheValuesFallback);
_directionIsRTL || (_sizeObserver ? _sizeObserver._getCurrentCacheValues(force)._directionIsRTL : booleanCacheValuesFallback);
const finalHeightIntrinsic =
_heightIntrinsic || (trinsicObserver ? trinsicObserver._getCurrentCacheValues(force)._heightIntrinsic : heightIntrinsicCacheValuesFallback);
_heightIntrinsic || (_trinsicObserver ? _trinsicObserver._getCurrentCacheValues(force)._heightIntrinsic : booleanCacheValuesFallback);
const checkOption: LifecycleCheckOption = (path) => ({
_value: getPropByPath(options, path),
_changed: force || getPropByPath(changedOptions, path) !== undefined,
@@ -180,6 +143,11 @@ export const createLifecycleHub = (options: OSOptions, structureSetup: Structure
const scrollOffsetX = adjustScrollOffset && scrollLeft(_viewport);
const scrollOffsetY = adjustScrollOffset && scrollTop(_viewport);
// place before updating lifecycles because of possible flushing of debounce
if (_updateObserverOptions) {
_updateObserverOptions(checkOption);
}
each(lifecycles, (lifecycle) => {
const {
_sizeChanged: adaptiveSizeChanged,
@@ -217,58 +185,7 @@ export const createLifecycleHub = (options: OSOptions, structureSetup: Structure
options.callbacks.onUpdated();
}
};
const onSizeChanged = (directionIsRTL?: CacheValues<boolean>) => {
const sizeChanged = !directionIsRTL;
updateLifecycles({
_directionIsRTL: directionIsRTL,
_sizeChanged: sizeChanged,
});
};
const onTrinsicChanged = (heightIntrinsic: CacheValues<boolean>) => {
updateLifecycles({
_heightIntrinsic: heightIntrinsic,
});
};
const onHostMutation = () => {
// TODO: rAF only here because IE
requestAnimationFrame(() => {
updateLifecycles({
_hostMutation: true,
});
});
};
const onContentMutation = () => {
// TODO: rAF only here because IE
requestAnimationFrame(() => {
updateLifecycles({
_contentMutation: true,
});
});
};
const trinsicObserver = (_content || !_flexboxGlue) && createTrinsicObserver(_host, onTrinsicChanged);
const sizeObserver = createSizeObserver(_host, onSizeChanged, { _appear: true, _direction: !_nativeScrollbarStyling });
const hostMutationObserver = createDOMObserver(_host, false, onHostMutation, {
_styleChangingAttributes: attrs,
_attributes: attrs,
_ignoreTargetChange: ignoreTargetChange,
});
const contentMutationObserver = createDOMObserver(_content || _viewport, true, onContentMutation, {
_styleChangingAttributes: attrs,
_attributes: attrs,
_eventContentChange: options!.updating!.elementEvents,
_nestedTargetSelector: hostSelector,
_ignoreContentChange: (mutation, isNestedTarget) => {
const { target, attributeName } = mutation;
return isNestedTarget
? false
: attributeName
? liesBetween(target as Element, hostSelector, viewportSelector) || liesBetween(target as Element, hostSelector, contentSelector)
: false;
},
_ignoreNestedTargetChange: ignoreTargetChange,
});
const { _sizeObserver, _trinsicObserver, _updateObserverOptions } = lifecycleHubOservers(instance, updateLifecycles);
const update = (changedOptions?: Partial<OSOptions> | null, force?: boolean) => {
updateLifecycles(null, changedOptions, force);
@@ -0,0 +1,141 @@
import { CacheValues, diffClass, debounce, isArray, isNumber } from 'support';
import { getEnvironment } from 'environment';
import { createSizeObserver, SizeObserverCallbackParams } from 'observers/sizeObserver';
import { createTrinsicObserver } from 'observers/trinsicObserver';
import { createDOMObserver, DOMObserver } from 'observers/domObserver';
import { LifecycleHub, LifecycleCheckOption, LifecycleUpdateHints } from 'lifecycles/lifecycleHub';
//const hostSelector = `.${classNameHost}`;
// TODO: observer textarea attrs if textarea
// TODO: tabindex, etc. attributes for viewport
// TODO: test _ignoreContentChange & _ignoreNestedTargetChange for content dom observer
// TODO: test _ignoreTargetChange for target dom observer
//const viewportSelector = `.${classNameViewport}`;
//const contentSelector = `.${classNameContent}`;
const ignorePrefix = 'os-';
const viewportAttrsFromTarget = ['tabindex'];
const baseStyleChangingAttrsTextarea = ['wrap', 'cols', 'rows'];
const baseStyleChangingAttrs = ['id', 'class', 'style', 'open'];
const ignoreTargetChange = (target: Node, attrName: string, oldValue: string | null, newValue: string | null) => {
if (attrName === 'class' && oldValue && newValue) {
const diff = diffClass(oldValue, newValue);
return !!diff.find((addedOrRemovedClass) => addedOrRemovedClass.indexOf(ignorePrefix) !== 0);
}
return false;
};
export const lifecycleHubOservers = (instance: LifecycleHub, updateLifecycles: (updateHints?: Partial<LifecycleUpdateHints> | null) => unknown) => {
let debounceTimeout: number | false | undefined;
let debounceMaxDelay: number | false | undefined;
const { _structureSetup } = instance;
const { _targetObj, _targetCtx } = _structureSetup;
const { _host, _viewport, _content } = _targetObj;
const { _isTextarea } = _targetCtx;
const { _nativeScrollbarStyling, _flexboxGlue } = getEnvironment();
const contentMutationObserverAttr = _isTextarea ? baseStyleChangingAttrsTextarea : baseStyleChangingAttrs.concat(baseStyleChangingAttrsTextarea);
const updateLifecyclesWithDebouncedAdaptiveUpdateHints = debounce(updateLifecycles as (updateHints: Partial<LifecycleUpdateHints>) => any, {
_timeout: () => debounceTimeout,
_maxDelay: () => debounceMaxDelay,
_mergeParams(prev, curr) {
const { _sizeChanged: prevSizeChanged, _hostMutation: prevHostMutation, _contentMutation: prevContentMutation } = prev[0];
const { _sizeChanged: currSizeChanged, _hostMutation: currvHostMutation, _contentMutation: currContentMutation } = curr[0];
const merged: [Partial<LifecycleUpdateHints>] = [
{
_sizeChanged: prevSizeChanged || currSizeChanged,
_hostMutation: prevHostMutation || currvHostMutation,
_contentMutation: prevContentMutation || currContentMutation,
},
];
return merged;
},
});
const onTrinsicChanged = (heightIntrinsic: CacheValues<boolean>) => {
updateLifecycles({
_heightIntrinsic: heightIntrinsic,
});
};
const onSizeChanged = ({ _sizeChanged, _directionIsRTLCache, _appear }: SizeObserverCallbackParams) => {
const updateFn = !_sizeChanged || _appear ? updateLifecycles : updateLifecyclesWithDebouncedAdaptiveUpdateHints;
updateFn({
_sizeChanged,
_directionIsRTL: _directionIsRTLCache,
});
};
const onContentMutation = (contentChangedTroughEvent: boolean) => {
// if contentChangedTroughEvent is true its already debounced
const updateFn = contentChangedTroughEvent ? updateLifecycles : updateLifecyclesWithDebouncedAdaptiveUpdateHints;
updateFn({
_contentMutation: true,
});
};
const onHostMutation = updateLifecyclesWithDebouncedAdaptiveUpdateHints.bind(0, {
_hostMutation: true,
}) as () => any;
const trinsicObserver = (_content || !_flexboxGlue) && createTrinsicObserver(_host, onTrinsicChanged);
const sizeObserver = createSizeObserver(_host, onSizeChanged, { _appear: true, _direction: !_nativeScrollbarStyling });
const hostMutationObserver = createDOMObserver(_host, false, onHostMutation, {
_styleChangingAttributes: baseStyleChangingAttrs,
_attributes: baseStyleChangingAttrs,
_ignoreTargetChange: ignoreTargetChange,
});
let contentMutationObserver: DOMObserver | undefined;
const updateOptions = (checkOption: LifecycleCheckOption) => {
const { _value: elementEvents, _changed: elementEventsChanged } = checkOption<Array<[string, string]> | null>('updating.elementEvents');
const { _value: attributes, _changed: attributesChanged } = checkOption<string[] | null>('updating.attributes');
const { _value: debounce, _changed: debounceChanged } = checkOption<Array<number> | number | null>('updating.debounce');
const updateContentMutationObserver = elementEventsChanged || attributesChanged;
if (updateContentMutationObserver) {
if (contentMutationObserver) {
contentMutationObserver._update();
contentMutationObserver._destroy();
}
contentMutationObserver = createDOMObserver(_content || _viewport, true, onContentMutation, {
_styleChangingAttributes: contentMutationObserverAttr.concat(attributes || []),
_attributes: contentMutationObserverAttr.concat(attributes || []),
_eventContentChange: elementEvents,
_ignoreNestedTargetChange: ignoreTargetChange,
//_nestedTargetSelector: hostSelector,
/*
_ignoreContentChange: (mutation, isNestedTarget) => {
const { target, attributeName } = mutation;
return isNestedTarget
? false
: attributeName
? liesBetween(target as Element, hostSelector, viewportSelector) || liesBetween(target as Element, hostSelector, contentSelector)
: false;
},
*/
});
}
if (debounceChanged) {
updateLifecyclesWithDebouncedAdaptiveUpdateHints._flush();
if (isArray(debounce)) {
const timeout = debounce[0];
const maxWait = debounce[1];
debounceTimeout = isNumber(timeout) ? timeout : false;
debounceMaxDelay = isNumber(maxWait) ? maxWait : false;
} else if (isNumber(debounce)) {
debounceTimeout = debounce;
debounceMaxDelay = false;
} else {
debounceTimeout = false;
debounceMaxDelay = false;
}
}
};
return {
_trinsicObserver: trinsicObserver,
_sizeObserver: sizeObserver,
_updateObserverOptions: updateOptions,
};
};
@@ -13,12 +13,11 @@ import {
find,
push,
isUndefined,
isFunction,
} from 'support';
type StringNullUndefined = string | null | undefined;
type DOMContentObserverCallback = (contentChanged: boolean) => any;
type DOMContentObserverCallback = (contentChangedTroughEvent: boolean) => any;
type DOMTargetObserverCallback = (targetChangedAttrs: string[], targetStyleChanged: boolean) => any;
@@ -38,18 +37,7 @@ interface DOMTargetObserverOptions extends DOMObserverOptionsBase {
_ignoreTargetChange?: DOMObserverIgnoreTargetChange; // a function which will prevent marking certain attributes as changed if it returns true
}
interface DOMObserverBase {
_destroy: () => void;
_update: () => void;
}
interface DOMContentObserver extends DOMObserverBase {
_updateEventContentChange: (newEventContentChange?: DOMObserverEventContentChange) => void;
}
interface DOMTargetObserver extends DOMObserverBase {}
type ContentChangeArrayItem = [StringNullUndefined, ((elms: Node[]) => StringNullUndefined) | StringNullUndefined] | null | undefined;
type ContentChangeArrayItem = [StringNullUndefined, StringNullUndefined] | null | undefined;
export type DOMObserverEventContentChange = Array<ContentChangeArrayItem> | false | null | undefined;
@@ -57,7 +45,7 @@ export type DOMObserverIgnoreContentChange = (
mutation: MutationRecord,
isNestedTarget: boolean,
domObserverTarget: HTMLElement,
domObserverOptions: DOMContentObserverOptions | undefined
domObserverOptions?: DOMContentObserverOptions
) => boolean;
export type DOMObserverIgnoreTargetChange = (
@@ -73,10 +61,10 @@ export type DOMObserverCallback<ContentObserver extends boolean> = ContentObserv
export type DOMObserverOptions<ContentObserver extends boolean> = ContentObserver extends true ? DOMContentObserverOptions : DOMTargetObserverOptions;
export type DOMObserver<ContentObserver extends boolean> = ContentObserver extends true ? DOMContentObserver : DOMTargetObserver;
// const styleChangingAttributes = ['id', 'class', 'style', 'open'];
// const mutationObserverAttrsTextarea = ['wrap', 'cols', 'rows'];
export interface DOMObserver {
_destroy: () => void;
_update: () => void;
}
/**
* Creates a set of helper functions to observe events of elements inside the target element.
@@ -87,7 +75,6 @@ export type DOMObserver<ContentObserver extends boolean> = ContentObserver exten
*/
const createEventContentChange = (target: Element, eventContentChange: DOMObserverEventContentChange, callback: (...args: any) => any) => {
let map: Map<Node, string> | undefined;
let eventContentChangeRef: DOMObserverEventContentChange;
const _destroy = () => {
if (map) {
map.forEach((eventName: string, elm: Node) => off(elm, eventName, callback));
@@ -95,16 +82,15 @@ const createEventContentChange = (target: Element, eventContentChange: DOMObserv
}
};
const _updateElements = (getElements?: (selector: string) => Node[]) => {
if (map && eventContentChangeRef) {
const eventElmList = eventContentChangeRef.reduce<Array<[Node[], string]>>((arr, item) => {
if (map && eventContentChange) {
const eventElmList = eventContentChange.reduce<Array<[Node[], string]>>((arr, item) => {
if (item) {
const selector = item[0];
const eventNames = item[1];
const elements = eventNames && selector && (getElements ? getElements(selector) : find(selector, target));
const parsedEventNames = isFunction(eventNames) ? eventNames(elements) : eventNames;
if (elements && elements.length && parsedEventNames && isString(parsedEventNames)) {
push(arr, [elements, parsedEventNames.trim()], true);
if (elements && elements.length && eventNames && isString(eventNames)) {
push(arr, [elements, eventNames.trim()], true);
}
}
return arr;
@@ -128,21 +114,16 @@ const createEventContentChange = (target: Element, eventContentChange: DOMObserv
);
}
};
const _updateEventContentChange = (newEventContentChange: DOMObserverEventContentChange) => {
map = map || new Map<Node, string>();
eventContentChangeRef = newEventContentChange;
_destroy();
_updateElements();
};
if (eventContentChange) {
_updateEventContentChange(eventContentChange);
map = map || new Map<Node, string>();
_destroy();
_updateElements();
}
return {
_destroy,
_updateElements,
_updateEventContentChange,
};
};
@@ -159,7 +140,7 @@ export const createDOMObserver = <ContentObserver extends boolean>(
isContentObserver: ContentObserver,
callback: DOMObserverCallback<ContentObserver>,
options?: DOMObserverOptions<ContentObserver>
): DOMObserver<ContentObserver> => {
): DOMObserver => {
let isConnected = false;
const {
_attributes,
@@ -170,18 +151,17 @@ export const createDOMObserver = <ContentObserver extends boolean>(
_ignoreNestedTargetChange,
_ignoreContentChange,
} = (options as DOMContentObserverOptions & DOMTargetObserverOptions) || {};
const {
_destroy: destroyEventContentChange,
_updateElements: updateEventContentChangeElements,
_updateEventContentChange: updateEventContentChange,
} = createEventContentChange(
const { _destroy: destroyEventContentChange, _updateElements: updateEventContentChangeElements } = createEventContentChange(
target,
isContentObserver && _eventContentChange,
debounce(() => {
if (isConnected) {
(callback as DOMContentObserverCallback)(true);
}
}, 84)
debounce(
() => {
if (isConnected) {
(callback as DOMContentObserverCallback)(true);
}
},
{ _timeout: 33, _maxDelay: 99 }
)
);
// MutationObserver
@@ -243,7 +223,7 @@ export const createDOMObserver = <ContentObserver extends boolean>(
}
if (isContentObserver) {
contentChanged && (callback as DOMContentObserverCallback)(contentChanged);
contentChanged && (callback as DOMContentObserverCallback)(false);
} else if (!isEmptyArray(targetChangedAttrs) || targetStyleChanged) {
(callback as DOMTargetObserverCallback)(targetChangedAttrs, targetStyleChanged);
}
@@ -269,13 +249,10 @@ export const createDOMObserver = <ContentObserver extends boolean>(
isConnected = false;
}
},
_updateEventContentChange: (newEventContentChange?: DOMObserverEventContentChange) => {
updateEventContentChange(isConnected && isContentObserver && newEventContentChange);
},
_update: () => {
if (isConnected) {
observerCallback(mutationObserver.takeRecords());
}
},
} as DOMObserver<ContentObserver>;
};
};
@@ -33,7 +33,16 @@ import {
classNameSizeObserverListenerItemFinal,
} from 'classnames';
export type SizeObserverOptions = { _direction?: boolean; _appear?: boolean };
export interface SizeObserverOptions {
_direction?: boolean;
_appear?: boolean;
}
export interface SizeObserverCallbackParams {
_sizeChanged: boolean;
_directionIsRTLCache?: CacheValues<boolean>;
_appear?: boolean;
}
export interface SizeObserver {
_destroy(): void;
@@ -73,7 +82,7 @@ const domRectHasDimensions = (rect?: DOMRectReadOnly) => rect && (rect.height ||
*/
export const createSizeObserver = (
target: HTMLElement,
onSizeChangedCallback: (directionIsRTLCache?: CacheValues<boolean>) => any,
onSizeChangedCallback: (params: SizeObserverCallbackParams) => any,
options?: SizeObserverOptions
): SizeObserver => {
const { _direction: observeDirectionChange = false, _appear: observeAppearChange = false } = options || {};
@@ -90,31 +99,44 @@ export const createSizeObserver = (
(!domRectHasDimensions(currVal) && domRectHasDimensions(newVal))
),
});
const onSizeChangedCallbackProxy = (sizeChangedContext?: CacheValues<boolean> | ResizeObserverEntry[] | Event) => {
const onSizeChangedCallbackProxy = (sizeChangedContext?: CacheValues<boolean> | ResizeObserverEntry[] | Event | boolean) => {
const hasDirectionCache = sizeChangedContext && isBoolean((sizeChangedContext as CacheValues<boolean>)._value);
let skip = false;
let appear: boolean | number | undefined = false;
let doDirectionScroll = true; // always true if sizeChangedContext is Event (appear callback or RO. Polyfill)
// if triggered from RO.
if (isArray(sizeChangedContext) && sizeChangedContext.length > 0) {
const { _previous, _value, _changed } = updateResizeObserverContentRectCache(0, sizeChangedContext.pop()!.contentRect);
skip = !_previous || !domRectHasDimensions(_value); // skip on initial RO. call or if display is none
doDirectionScroll = !skip && _changed; // direction scroll when not skipping and changing from display: none to block, false otherwise
const { _previous, _value } = updateResizeObserverContentRectCache(0, sizeChangedContext.pop()!.contentRect);
const hasDimensions = domRectHasDimensions(_value);
const hadDimensions = domRectHasDimensions(_previous);
skip = !_previous || !hasDimensions; // skip on initial RO. call or if display is none
appear = !hadDimensions && hasDimensions;
doDirectionScroll = !skip; // direction scroll when not skipping
}
// else if its triggered with DirectionCache
else if (hasDirectionCache) {
doDirectionScroll = (sizeChangedContext as CacheValues<boolean>)._changed; // direction scroll when DirectionCache changed, false otherwise
}
// else if it triggered with appear from polyfill
else {
appear = sizeChangedContext === true;
}
if (observeDirectionChange) {
if (observeDirectionChange && doDirectionScroll) {
const rtl = hasDirectionCache ? (sizeChangedContext as CacheValues<boolean>)._value : directionIsRTL(sizeObserver);
scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount);
scrollTop(sizeObserver, scrollAmount);
}
if (!skip) {
onSizeChangedCallback(hasDirectionCache ? (sizeChangedContext as CacheValues<boolean>) : undefined);
onSizeChangedCallback({
_sizeChanged: !hasDirectionCache,
_directionIsRTLCache: hasDirectionCache ? (sizeChangedContext as CacheValues<boolean>) : undefined,
_appear: !!appear,
});
}
};
const offListeners: (() => void)[] = [];
@@ -147,11 +169,11 @@ export const createSizeObserver = (
scrollLeft(shrinkElement, scrollAmount);
scrollTop(shrinkElement, scrollAmount);
};
const onResized = () => {
const onResized = (appear?: unknown) => {
rAFId = 0;
if (isDirty) {
cacheSize = currSize;
onSizeChangedCallbackProxy();
onSizeChangedCallbackProxy(appear === true);
}
};
const onScroll = (scrollEvent?: Event | false) => {
@@ -166,7 +188,7 @@ export const createSizeObserver = (
rAFId = rAF!(onResized);
}
} else {
onResized();
onResized(scrollEvent === false);
}
reset();
+9 -7
View File
@@ -35,9 +35,8 @@ export interface OSOptions {
paddingAbsolute: boolean;
updating: {
elementEvents: Array<[string, string]> | null;
contentMutationDebounce: number;
hostMutationDebounce: number;
resizeDebounce: number;
attributes: string[] | null;
debounce: number | [number, number] | null;
};
overflow: {
x: OverflowBehavior;
@@ -109,6 +108,7 @@ export interface UpdatedArgs {
}
const numberAllowedValues: OptionsTemplateValue<number> = oTypes.number;
const arrayNullValues: OptionsTemplateValue<Array<unknown> | null> = [oTypes.array, oTypes.null];
const stringArrayNullAllowedValues: OptionsTemplateValue<string | ReadonlyArray<string> | null> = [oTypes.string, oTypes.array, oTypes.null];
const booleanTrueTemplate: OptionsWithOptionsTemplateValue<boolean> = [true, oTypes.boolean];
const booleanFalseTemplate: OptionsWithOptionsTemplateValue<boolean> = [false, oTypes.boolean];
@@ -137,10 +137,12 @@ const defaultOptionsWithTemplate: OptionsWithOptionsTemplate<OSOptions> = {
resize: ['none', resizeAllowedValues], // none || both || horizontal || vertical || n || b || h || v
paddingAbsolute: booleanFalseTemplate, // true || false
updating: {
elementEvents: [[['img', 'load']], [oTypes.array, oTypes.null]], // array of tuples || null
contentMutationDebounce: [80, numberAllowedValues], // number
hostMutationDebounce: [0, numberAllowedValues], // number
resizeDebounce: [0, numberAllowedValues], // number
elementEvents: [[['img', 'load']], arrayNullValues], // array of tuples || null
attributes: [null, arrayNullValues],
debounce: [
[0, 33],
[oTypes.number, oTypes.array, oTypes.null],
], // number || number array || null
},
overflow: {
x: ['scroll', overflowAllowedValues], // visible-hidden || visible-scroll || hidden || scroll || v-h || v-s || h || s
@@ -1,36 +1,103 @@
import { isNumber } from 'support/utils/types';
import { cAF, rAF } from 'support/compatibility/apis';
import { isNumber, isFunction } from 'support/utils/types';
import { rAF, cAF } from 'support/compatibility/apis';
const setT = window.setTimeout;
const clearTimeouts = (id: number | undefined) => {
id && window.clearTimeout(id);
id && cAF!(id);
};
type DebounceTiming = number | false | null | undefined;
export interface DebounceOptions<FunctionToDebounce extends (...args: any) => any> {
/**
* The timeout for debouncing. If null, no debounce is applied.
*/
_timeout?: DebounceTiming | (() => DebounceTiming);
/**
* A maximum amount of ms. before the function will be called even with debounce.
*/
_maxDelay?: DebounceTiming | (() => DebounceTiming);
/**
* Function which merges parameters for each canceled debounce.
* If parameters can't be merged the function will return null, otherwise it returns the merged parameters.
*/
_mergeParams?: (
prev: Parameters<FunctionToDebounce>,
curr: Parameters<FunctionToDebounce>
) => Parameters<FunctionToDebounce> | false | null | undefined;
}
export interface Debounced<FunctionToDebounce extends (...args: any) => any> {
(...args: Parameters<FunctionToDebounce>): ReturnType<FunctionToDebounce>;
_flush(): void;
}
export const noop = () => {}; // eslint-disable-line
/**
* Debounces the given function either with a timeout or a animation frame.
* @param functionToDebounce The function which shall be debounced.
* @param timeout The timeout for debouncing. If 0 or lower animation frame is used for debouncing, a timeout otherwise.
* @param maxWait A maximum amount of ms. before the function will be called even with debounce.
* @param options Options for debouncing.
*/
export const debounce = (functionToDebounce: (...args: any) => any, timeout?: number, maxWait?: number) => {
let timeoutId: number | void;
let lastCallTime: number;
const hasTimeout = isNumber(timeout) && timeout > 0;
const hasMaxWait = isNumber(maxWait) && maxWait > 0;
const cancel = hasTimeout ? window.clearTimeout : cAF!;
const set = hasTimeout ? window.setTimeout : rAF!;
const setFn = function (args: IArguments) {
lastCallTime = hasMaxWait ? performance.now() : 0;
timeoutId && cancel(timeoutId);
export const debounce = <FunctionToDebounce extends (...args: any) => any>(
functionToDebounce: FunctionToDebounce,
options: DebounceOptions<FunctionToDebounce>
): Debounced<FunctionToDebounce> => {
let timeoutId: number | undefined;
let maxTimeoutId: number | undefined;
let prevArguments: Parameters<FunctionToDebounce> | null | undefined;
let latestArguments: Parameters<FunctionToDebounce> | null | undefined;
const { _timeout, _maxDelay, _mergeParams } = options;
const invokeFunctionToDebounce = function (args: IArguments) {
clearTimeouts(timeoutId);
clearTimeouts(maxTimeoutId);
maxTimeoutId = timeoutId = prevArguments = undefined;
// eslint-disable-next-line
// @ts-ignore
functionToDebounce.apply(this, args);
};
return function () {
// eslint-disable-next-line
// @ts-ignore
const boundSetFn = setFn.bind(this, arguments); // eslint-disable-line
const forceCall = hasMaxWait ? performance.now() - lastCallTime >= maxWait! : false;
const mergeParms = (curr: Parameters<FunctionToDebounce>): Parameters<FunctionToDebounce> | false | null | undefined =>
_mergeParams && prevArguments ? _mergeParams(prevArguments, curr) : curr;
timeoutId && cancel(timeoutId);
timeoutId = forceCall ? boundSetFn() : (set(boundSetFn, timeout!) as number);
const flush = () => {
if (timeoutId) {
invokeFunctionToDebounce(mergeParms(latestArguments!) || latestArguments!);
}
};
const debouncedFn = function () {
const args: Parameters<FunctionToDebounce> = arguments as Parameters<FunctionToDebounce>;
const finalTimeout = isFunction(_timeout) ? _timeout() : _timeout;
const hasTimeout = isNumber(finalTimeout) && finalTimeout >= 0;
if (hasTimeout) {
const finalMaxWait = isFunction(_maxDelay) ? _maxDelay() : _maxDelay;
const hasMaxWait = isNumber(finalMaxWait) && finalMaxWait >= 0;
const setTimeoutFn = finalTimeout! > 0 ? setT : rAF!;
const mergeParamsResult = mergeParms(args);
const invokedArgs = mergeParamsResult || args;
const boundInvoke = invokeFunctionToDebounce.bind(0, invokedArgs);
if (!mergeParamsResult) {
invokeFunctionToDebounce(prevArguments || args);
}
clearTimeouts(timeoutId);
timeoutId = setTimeoutFn(boundInvoke, finalTimeout as number) as number;
if (hasMaxWait && !maxTimeoutId) {
maxTimeoutId = setT(flush, finalMaxWait as number);
}
prevArguments = latestArguments = invokedArgs;
} else {
invokeFunctionToDebounce(args);
}
};
debouncedFn._flush = flush;
return debouncedFn as Debounced<FunctionToDebounce>;
};
@@ -4,24 +4,14 @@ import should from 'should';
import { generateSelectCallback, iterateSelect } from '@/testing-browser/Select';
import { timeout } from '@/testing-browser/timeout';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import {
appendChildren,
createDiv,
removeElements,
children,
isArray,
isNumber,
liesBetween,
hasClass,
addClass,
removeClass,
diffClass,
on,
} from 'support';
import { appendChildren, createDiv, removeElements, children, isArray, isNumber, liesBetween, addClass, removeClass, diffClass, on } from 'support';
import { createDOMObserver } from 'observers/domObserver';
type DOMContentObserverResult = boolean;
type DOMContentObserverResult = {
contentChange: boolean;
troughEvent: boolean;
};
type DOMTargetObserverResult = {
changedTargetAttrs: string[];
styleChanged: boolean;
@@ -34,6 +24,7 @@ interface SeparateChangeThrough {
const targetChangesCountSlot: HTMLElement | null = document.querySelector('#targetChanges');
const contentChangesCountSlot: HTMLElement | null = document.querySelector('#contentChanges');
const targetElm: HTMLElement | null = document.querySelector('#target');
const trargetContentElm: HTMLElement | null = document.querySelector('#target .content');
const targetElmContentElm: HTMLElement | null = document.querySelector('#content-host');
const contentElmAttrChange: HTMLElement | null = document.querySelector('#target .content-nest');
const contentBetweenElmAttrChange: HTMLElement | null = document.querySelector('#content-host .padding-nest-item');
@@ -67,7 +58,7 @@ const startBtn: HTMLButtonElement | null = document.querySelector('#start');
const hostSelector = '.host';
const ignorePrefix = 'ignore';
const attrs = ['id', 'class', 'style', 'open'];
const contentChangeArr: Array<[string, string | ((elms: Node[]) => string)]> = [['img', 'load']];
const contentChangeArr: Array<[string, string]> = [['img', 'load']];
const domTargetObserverObservations: DOMTargetObserverResult[] = [];
const domContentObserverObservations: DOMContentObserverResult[] = [];
@@ -115,44 +106,48 @@ const targetDomObserver = createDOMObserver(
}
);
const contentDomObserver = createDOMObserver(
document.querySelector('#target .content')!,
true,
(contentChanged: boolean) => {
should.equal(typeof contentChanged, 'boolean', 'The contentChanged parameter in a content dom observer must be a boolean.');
const createContentDomOserver = (eventContentChange: Array<[string | null | undefined, string | null | undefined] | null | undefined>) => {
return createDOMObserver(
trargetContentElm!,
true,
(contentChangedTroughEvent: boolean) => {
should.equal(typeof contentChangedTroughEvent, 'boolean', 'The contentChanged parameter in a content dom observer must be a boolean.');
domContentObserverObservations.push(contentChanged);
requestAnimationFrame(() => {
if (contentChangesCountSlot) {
contentChangesCountSlot.textContent = `${domContentObserverObservations.length}`;
}
});
},
{
_styleChangingAttributes: attrs,
_attributes: attrs,
_eventContentChange: contentChangeArr,
_nestedTargetSelector: hostSelector,
_ignoreContentChange: (mutation, isNestedTarget) => {
const { target, attributeName } = mutation;
return isNestedTarget ? false : attributeName ? liesBetween(target as Element, hostSelector, '.content') : false;
domContentObserverObservations.push({ contentChange: true, troughEvent: contentChangedTroughEvent });
requestAnimationFrame(() => {
if (contentChangesCountSlot) {
contentChangesCountSlot.textContent = `${domContentObserverObservations.length}`;
}
});
},
_ignoreNestedTargetChange: (target, attrName, oldValue, newValue) => {
if (attrName === 'class' && oldValue && newValue) {
const diff = diffClass(oldValue, newValue);
const ignore = diff.length === 1 && diff[0].startsWith(ignorePrefix);
return ignore;
}
return false;
},
// @ts-ignore
_ignoreTargetChange: () => {
// if param: isContentObserver = true, this function should never be called.
should.ok(false, 'A content dom observer must not call the _ignoreTargetChange method.');
return true;
},
}
);
{
_styleChangingAttributes: attrs,
_attributes: attrs,
_eventContentChange: eventContentChange,
_nestedTargetSelector: hostSelector,
_ignoreContentChange: (mutation, isNestedTarget) => {
const { target, attributeName } = mutation;
return isNestedTarget ? false : attributeName ? liesBetween(target as Element, hostSelector, '.content') : false;
},
_ignoreNestedTargetChange: (target, attrName, oldValue, newValue) => {
if (attrName === 'class' && oldValue && newValue) {
const diff = diffClass(oldValue, newValue);
const ignore = diff.length === 1 && diff[0].startsWith(ignorePrefix);
return ignore;
}
return false;
},
// @ts-ignore
_ignoreTargetChange: () => {
// if param: isContentObserver = true, this function should never be called.
should.ok(false, 'A content dom observer must not call the _ignoreTargetChange method.');
return true;
},
}
);
};
let contentDomObserver = createContentDomOserver(contentChangeArr);
const getTotalObservations = () => domTargetObserverObservations.length + domContentObserverObservations.length;
const getLast = <T>(arr: T[], indexFromLast = 0): T => arr[arr.length - 1 - indexFromLast] || ({} as T);
@@ -277,7 +272,7 @@ const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMCo
if (addChangeThrough) {
const contentChanged = getLast(addChangeThrough);
await waitForOrFailTest(() => {
should.equal(contentChanged, true, 'Adding an content element must result in a content change.');
should.deepEqual(contentChanged, { contentChange: true, troughEvent: false }, 'Adding an content element must result in a content change.');
});
}
};
@@ -297,7 +292,11 @@ const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMCo
if (removeChangeThrough) {
const contentChanged = getLast(removeChangeThrough);
should.equal(contentChanged, true, 'Removing an content element must result in a content change.');
should.deepEqual(
contentChanged,
{ contentChange: true, troughEvent: false },
'Removing an content element must result in a content change.'
);
}
});
}
@@ -361,10 +360,14 @@ const addRemoveImgElmsFn = async () => {
compare(2);
const previousContentChanged = getLast(domContentObserverObservations, 1);
should.equal(previousContentChanged, true, 'Adding an content image must result in a content change.');
should.deepEqual(
previousContentChanged,
{ contentChange: true, troughEvent: false },
'Adding an content image must result in a content change.'
);
const lastContentChanged = getLast(domContentObserverObservations);
should.equal(lastContentChanged, true, 'The images load event must result in a content change.');
should.deepEqual(lastContentChanged, { contentChange: true, troughEvent: true }, 'The images load event must result in a content change.');
});
};
@@ -375,21 +378,20 @@ const addRemoveImgElmsFn = async () => {
// test event content change debounce
const addMultiple = async () => {
const { before, after, compare } = changedThrough(domContentObserverObservations);
const addMultipleItem = () => {
const genImage = () => {
const img = new Image(1, 1);
img.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
const imgHolder = createDiv('img');
appendChildren(imgHolder, img);
appendChildren(imgElmsSlot, imgHolder);
return imgHolder;
};
before();
await timeout(250);
addMultipleItem();
addMultipleItem();
addMultipleItem();
before();
appendChildren(imgElmsSlot, [genImage(), genImage(), genImage()]);
await timeout(250);
@@ -398,20 +400,27 @@ const addRemoveImgElmsFn = async () => {
compare(2);
const previousContentChanged = getLast(domContentObserverObservations, 1);
should.equal(previousContentChanged, true, 'Adding mutliple content images must result in a single content change. (debounced)');
should.deepEqual(
previousContentChanged,
{ contentChange: true, troughEvent: false },
'Adding mutliple content images must result in a single content change. (debounced)'
);
const lastContentChanged = getLast(domContentObserverObservations);
should.equal(lastContentChanged, true, 'Multiple images load events must result in a single cintent change. (debounced)');
should.deepEqual(
lastContentChanged,
{ contentChange: true, troughEvent: true },
'Multiple images load events must result in a single cintent change. (debounced)'
);
});
};
await addMultiple();
// remove load event from image test
const addChanged = async (
newEventContentChange: Array<[string | null | undefined, (() => string | null | undefined) | string | null | undefined] | null | undefined>
) => {
contentDomObserver._updateEventContentChange(newEventContentChange);
const addChanged = async (newEventContentChange: Array<[string | null | undefined, string | null | undefined] | null | undefined>) => {
contentDomObserver._destroy();
contentDomObserver = createContentDomOserver(newEventContentChange);
const img = new Image(1, 1);
img.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
@@ -430,7 +439,8 @@ const addRemoveImgElmsFn = async () => {
compare(1);
});
contentDomObserver._updateEventContentChange(contentChangeArr);
contentDomObserver._destroy();
contentDomObserver = createContentDomOserver(contentChangeArr);
};
await addChanged([
@@ -440,9 +450,6 @@ const addRemoveImgElmsFn = async () => {
['img', undefined],
[null, null],
[undefined, undefined],
['img', () => 'hi'],
['img', () => null],
['img', () => undefined],
null,
undefined,
]);
@@ -471,7 +478,11 @@ const addRemoveTransitionElmsFn = async () => {
compareTransition(expectTransitionEndContentChange ? 2 : 1); // 2 because 1: added class mutation and 2: transition end event
const contentChanged = getLast(domContentObserverObservations);
should.equal(contentChanged, true, 'The transitionend event must trigger a event content change.');
should.deepEqual(
contentChanged,
{ contentChange: true, troughEvent: expectTransitionEndContentChange },
'The transitionend event must trigger a event content change.'
);
resolve(1);
});
},
@@ -495,11 +506,16 @@ const addRemoveTransitionElmsFn = async () => {
compare(1);
const contentChanged = getLast(domContentObserverObservations);
should.equal(contentChanged, true, 'Adding an content element (transition) must result in a content change.');
should.deepEqual(
contentChanged,
{ contentChange: true, troughEvent: false },
'Adding an content element (transition) must result in a content change.'
);
});
await startTransition(elm, expectTransitionEndContentChange && true);
contentDomObserver._updateEventContentChange(contentChangeArr);
contentDomObserver._destroy();
contentDomObserver = createContentDomOserver(contentChangeArr);
await startTransition(elm, expectTransitionEndContentChange && false);
removeElements(elm);
@@ -509,19 +525,8 @@ const addRemoveTransitionElmsFn = async () => {
await add(false);
contentDomObserver._updateEventContentChange(
contentChangeArr.concat([
[
'.transition',
(elms) => {
elms.forEach((elm) => {
should.equal(hasClass(elm as Element, 'transition'), true, 'Every checked element must match the correpsonding selector.'); // in this case "".transition"
});
return 'transitionend';
},
],
])
);
contentDomObserver._destroy();
contentDomObserver = createContentDomOserver(contentChangeArr.concat([['.transition', 'transitionend']]));
await add(true);
};
@@ -562,7 +567,11 @@ const iterateTargetAttrChange = async () => {
const iterateContentAttrChange = async () => {
await iterateAttrChange(setContentAttr, domContentObserverObservations, (observation) => {
const contentChanged = observation;
should.equal(contentChanged, true, 'A attribute change inside the content must trigger a content change for a DOMContentObserver.');
should.deepEqual(
contentChanged,
{ contentChange: true, troughEvent: false },
'A attribute change inside the content must trigger a content change for a DOMContentObserver.'
);
});
await iterateAttrChange(setFilteredContentAttr);
};
@@ -573,7 +582,11 @@ const iterateContentBetweenAttrChange = async () => {
const iterateContentHostElmAttrChange = async () => {
await iterateAttrChange(setContentHostElmAttr, domContentObserverObservations, (observation) => {
const contentChanged = observation;
should.equal(contentChanged, true, 'A attribute change for a nested target must trigger a content change for a DOMContentObserver.');
should.deepEqual(
contentChanged,
{ contentChange: true, troughEvent: false },
'A attribute change for a nested target must trigger a content change for a DOMContentObserver.'
);
});
await iterateAttrChange(setFilteredContentHostElmAttr);
};
@@ -626,11 +639,9 @@ const start = async () => {
targetDomObserver._destroy();
targetDomObserver._update();
contentDomObserver._updateEventContentChange([]);
contentDomObserver._update();
contentDomObserver._destroy();
contentDomObserver._destroy();
contentDomObserver._updateEventContentChange([]);
contentDomObserver._update();
};
@@ -37,12 +37,14 @@ const preInitChildren = targetElm?.children.length;
const sizeObserver = createSizeObserver(
targetElm as HTMLElement,
(directionIsRTLCache?: any) => {
if (directionIsRTLCache) {
directionIterations += 1;
} else {
({ _directionIsRTLCache, _sizeChanged, _appear }) => {
if (_sizeChanged) {
sizeIterations += 1;
}
if (_directionIsRTLCache) {
directionIterations += 1;
}
requestAnimationFrame(() => {
if (resizesSlot) {
resizesSlot.textContent = (directionIterations + sizeIterations).toString();
+1 -1
View File
@@ -268,7 +268,7 @@ const rollupConfig = (config = {}, { project = process.cwd(), overwrite = {}, si
plugins: [
...(output.plugins || []),
rollupTerser({
ecma: 8,
ecma: esm ? 2015 : 5,
safari10: true,
mangle: {
safari10: true,