mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-10 16:52:26 +03:00
add debounce and updating options
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user