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