mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-05 18:22:27 +03:00
Merge branch 'v2.0.0' of https://github.com/KingSora/OverlayScrollbars into v2.0.0
This commit is contained in:
@@ -67,6 +67,7 @@
|
||||
"test": "yarn workspaces run test",
|
||||
"test:jsdom": "yarn workspaces run test:jsdom",
|
||||
"test:browser": "yarn workspaces run test:browser",
|
||||
"test:browser:quick": "yarn workspaces run test:browser:quick",
|
||||
"test:browser-dev": "yarn workspaces run test:browser-dev",
|
||||
"build": "yarn workspaces run build",
|
||||
"lint": "npx eslint --fix ."
|
||||
|
||||
+603
-538
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+687
-589
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@
|
||||
"test": "jest --coverage --runInBand --detectOpenHandles",
|
||||
"test:jsdom": "jest --coverage --runInBand --detectOpenHandles --selectProjects jsdom --testPathPattern",
|
||||
"test:browser": "jest --runInBand --detectOpenHandles --selectProjects browser --testPathPattern",
|
||||
"test:browser:quick": "jest --runInBand --detectOpenHandles --selectProjects browser --testPathIgnorePatterns=\"/node_modules/|/structureLifecycle/\"",
|
||||
"test:browser-dev": "jest --runInBand --detectOpenHandles --selectProjects browser-dev --testPathPattern",
|
||||
"build": "rollup -c"
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { LifecycleHub, LifecycleCheckOption, LifecycleUpdateHints } from 'lifecycles/lifecycleHub';
|
||||
export declare const lifecycleHubOservers: (instance: LifecycleHub, updateLifecycles: (updateHints?: Partial<LifecycleUpdateHints> | null | undefined) => unknown) => {
|
||||
_trinsicObserver: false | import("observers/trinsicObserver").TrinsicObserver;
|
||||
_sizeObserver: import("observers/sizeObserver").SizeObserver;
|
||||
_updateObserverOptions: (checkOption: LifecycleCheckOption) => void;
|
||||
};
|
||||
+8
-14
@@ -1,5 +1,5 @@
|
||||
declare type StringNullUndefined = string | null | undefined;
|
||||
declare type DOMContentObserverCallback = (contentChanged: boolean) => any;
|
||||
declare type DOMContentObserverCallback = (contentChangedTroughEvent: boolean) => any;
|
||||
declare type DOMTargetObserverCallback = (targetChangedAttrs: string[], targetStyleChanged: boolean) => any;
|
||||
interface DOMObserverOptionsBase {
|
||||
_attributes?: string[];
|
||||
@@ -14,22 +14,16 @@ interface DOMContentObserverOptions extends DOMObserverOptionsBase {
|
||||
interface DOMTargetObserverOptions extends DOMObserverOptionsBase {
|
||||
_ignoreTargetChange?: DOMObserverIgnoreTargetChange;
|
||||
}
|
||||
interface DOMObserverBase {
|
||||
_destroy: () => void;
|
||||
_update: () => void;
|
||||
}
|
||||
interface DOMContentObserver extends DOMObserverBase {
|
||||
_updateEventContentChange: (newEventContentChange?: DOMObserverEventContentChange) => void;
|
||||
}
|
||||
interface DOMTargetObserver extends DOMObserverBase {
|
||||
}
|
||||
declare type ContentChangeArrayItem = [StringNullUndefined, ((elms: Node[]) => StringNullUndefined) | StringNullUndefined] | null | undefined;
|
||||
declare type ContentChangeArrayItem = [StringNullUndefined, StringNullUndefined] | null | undefined;
|
||||
export declare type DOMObserverEventContentChange = Array<ContentChangeArrayItem> | false | null | undefined;
|
||||
export declare type DOMObserverIgnoreContentChange = (mutation: MutationRecord, isNestedTarget: boolean, domObserverTarget: HTMLElement, domObserverOptions: DOMContentObserverOptions | undefined) => boolean;
|
||||
export declare type DOMObserverIgnoreContentChange = (mutation: MutationRecord, isNestedTarget: boolean, domObserverTarget: HTMLElement, domObserverOptions?: DOMContentObserverOptions) => boolean;
|
||||
export declare type DOMObserverIgnoreTargetChange = (target: Node, attributeName: string, oldAttributeValue: string | null, newAttributeValue: string | null) => boolean;
|
||||
export declare type DOMObserverCallback<ContentObserver extends boolean> = ContentObserver extends true ? DOMContentObserverCallback : DOMTargetObserverCallback;
|
||||
export declare type DOMObserverOptions<ContentObserver extends boolean> = ContentObserver extends true ? DOMContentObserverOptions : DOMTargetObserverOptions;
|
||||
export declare type DOMObserver<ContentObserver extends boolean> = ContentObserver extends true ? DOMContentObserver : DOMTargetObserver;
|
||||
export interface DOMObserver {
|
||||
_destroy: () => void;
|
||||
_update: () => void;
|
||||
}
|
||||
/**
|
||||
* Creates a DOM observer which observes DOM changes to either the target element or its children.
|
||||
* @param target The element which shall be observed.
|
||||
@@ -38,5 +32,5 @@ export declare type DOMObserver<ContentObserver extends boolean> = ContentObserv
|
||||
* @param options The options for DOM change detection.
|
||||
* @returns A object which represents the instance of the DOM observer.
|
||||
*/
|
||||
export declare const createDOMObserver: <ContentObserver extends boolean>(target: HTMLElement, isContentObserver: ContentObserver, callback: DOMObserverCallback<ContentObserver>, options?: DOMObserverOptions<ContentObserver> | undefined) => DOMObserver<ContentObserver>;
|
||||
export declare const createDOMObserver: <ContentObserver extends boolean>(target: HTMLElement, isContentObserver: ContentObserver, callback: DOMObserverCallback<ContentObserver>, options?: DOMObserverOptions<ContentObserver> | undefined) => DOMObserver;
|
||||
export {};
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { CacheValues } from 'support';
|
||||
export declare type SizeObserverOptions = {
|
||||
export interface SizeObserverOptions {
|
||||
_direction?: boolean;
|
||||
_appear?: boolean;
|
||||
};
|
||||
}
|
||||
export interface SizeObserverCallbackParams {
|
||||
_sizeChanged: boolean;
|
||||
_directionIsRTLCache?: CacheValues<boolean>;
|
||||
_appear?: boolean;
|
||||
}
|
||||
export interface SizeObserver {
|
||||
_destroy(): void;
|
||||
_getCurrentCacheValues(force?: boolean): {
|
||||
@@ -16,4 +21,4 @@ export interface SizeObserver {
|
||||
* @param options The options for size detection, whether to observe also direction and appear.
|
||||
* @returns A object which represents the instance of the size observer.
|
||||
*/
|
||||
export declare const createSizeObserver: (target: HTMLElement, onSizeChangedCallback: (directionIsRTLCache?: CacheValues<boolean> | undefined) => any, options?: SizeObserverOptions | undefined) => SizeObserver;
|
||||
export declare const createSizeObserver: (target: HTMLElement, onSizeChangedCallback: (params: SizeObserverCallbackParams) => any, options?: SizeObserverOptions | undefined) => SizeObserver;
|
||||
|
||||
+2
-3
@@ -15,9 +15,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;
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
declare 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 declare const noop: () => void;
|
||||
/**
|
||||
* 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 declare const debounce: (functionToDebounce: (...args: any) => any, timeout?: number | undefined, maxWait?: number | undefined) => () => void;
|
||||
export declare const debounce: <FunctionToDebounce extends (...args: any) => any>(functionToDebounce: FunctionToDebounce, options: DebounceOptions<FunctionToDebounce>) => Debounced<FunctionToDebounce>;
|
||||
export {};
|
||||
|
||||
@@ -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