This commit is contained in:
Rene
2022-07-18 20:54:10 +02:00
6 changed files with 183 additions and 69 deletions
@@ -66,7 +66,10 @@ export type DOMObserverOptions<ContentObserver extends boolean> = ContentObserve
? DOMContentObserverOptions ? DOMContentObserverOptions
: DOMTargetObserverOptions; : DOMTargetObserverOptions;
export type DOMObserver = [destroy: () => void, update: () => void]; export type DOMObserver<ContentObserver extends boolean> = [
destroy: () => void,
update: () => void | false | Parameters<DOMObserverCallback<ContentObserver>>
];
type EventContentChangeUpdateElement = (getElements?: (selector: string) => Node[]) => void; type EventContentChangeUpdateElement = (getElements?: (selector: string) => Node[]) => void;
type EventContentChange = [destroy: () => void, updateElements: EventContentChangeUpdateElement]; type EventContentChange = [destroy: () => void, updateElements: EventContentChangeUpdateElement];
@@ -156,7 +159,7 @@ export const createDOMObserver = <ContentObserver extends boolean>(
isContentObserver: ContentObserver, isContentObserver: ContentObserver,
callback: DOMObserverCallback<ContentObserver>, callback: DOMObserverCallback<ContentObserver>,
options?: DOMObserverOptions<ContentObserver> options?: DOMObserverOptions<ContentObserver>
): DOMObserver => { ): DOMObserver<ContentObserver> => {
let isConnected = false; let isConnected = false;
const { const {
_attributes, _attributes,
@@ -166,16 +169,17 @@ export const createDOMObserver = <ContentObserver extends boolean>(
_ignoreTargetChange, _ignoreTargetChange,
_ignoreContentChange, _ignoreContentChange,
} = (options as DOMContentObserverOptions & DOMTargetObserverOptions) || {}; } = (options as DOMContentObserverOptions & DOMTargetObserverOptions) || {};
const debouncedEventContentChange = debounce(
() => {
if (isConnected) {
(callback as DOMContentObserverCallback)(true);
}
},
{ _timeout: 33, _maxDelay: 99 }
);
const [destroyEventContentChange, updateEventContentChangeElements] = createEventContentChange( const [destroyEventContentChange, updateEventContentChangeElements] = createEventContentChange(
target, target,
debounce( debouncedEventContentChange,
() => {
if (isConnected) {
(callback as DOMContentObserverCallback)(true);
}
},
{ _timeout: 33, _maxDelay: 99 }
),
_eventContentChange _eventContentChange
); );
@@ -183,7 +187,10 @@ export const createDOMObserver = <ContentObserver extends boolean>(
const finalAttributes = _attributes || []; const finalAttributes = _attributes || [];
const finalStyleChangingAttributes = _styleChangingAttributes || []; const finalStyleChangingAttributes = _styleChangingAttributes || [];
const observedAttributes = finalAttributes.concat(finalStyleChangingAttributes); const observedAttributes = finalAttributes.concat(finalStyleChangingAttributes);
const observerCallback = (mutations: MutationRecord[]) => { const observerCallback = (
mutations: MutationRecord[],
fromRecords?: true
): void | Parameters<DOMObserverCallback<ContentObserver>> => {
const ignoreTargetChange = _ignoreTargetChange || noop; const ignoreTargetChange = _ignoreTargetChange || noop;
const ignoreContentChange = _ignoreContentChange || noop; const ignoreContentChange = _ignoreContentChange || noop;
const targetChangedAttrs: string[] = []; const targetChangedAttrs: string[] = [];
@@ -244,12 +251,20 @@ export const createDOMObserver = <ContentObserver extends boolean>(
} }
if (isContentObserver) { if (isContentObserver) {
contentChanged && (callback as DOMContentObserverCallback)(false); !fromRecords && contentChanged && (callback as DOMContentObserverCallback)(false);
} else if (!isEmptyArray(targetChangedAttrs) || targetStyleChanged) { return [false] as Parameters<DOMObserverCallback<ContentObserver>>;
(callback as DOMTargetObserverCallback)(targetChangedAttrs, targetStyleChanged); }
if (!isEmptyArray(targetChangedAttrs) || targetStyleChanged) {
!fromRecords &&
(callback as DOMTargetObserverCallback)(targetChangedAttrs, targetStyleChanged);
return [targetChangedAttrs, targetStyleChanged] as Parameters<
DOMObserverCallback<ContentObserver>
>;
} }
}; };
const mutationObserver: MutationObserver = new MutationObserverConstructor!(observerCallback); const mutationObserver: MutationObserver = new MutationObserverConstructor!((mutations) =>
observerCallback(mutations)
);
// Connect // Connect
mutationObserver.observe(target, { mutationObserver.observe(target, {
@@ -272,7 +287,10 @@ export const createDOMObserver = <ContentObserver extends boolean>(
}, },
() => { () => {
if (isConnected) { if (isConnected) {
observerCallback(mutationObserver.takeRecords()); debouncedEventContentChange._flush();
const records = mutationObserver.takeRecords();
return !isEmptyArray(records) && observerCallback(records, true);
} }
}, },
]; ];
@@ -40,8 +40,6 @@ export interface SizeObserverCallbackParams {
export type DestroySizeObserver = () => void; export type DestroySizeObserver = () => void;
const animationStartEventName = 'animationstart';
const scrollEventName = 'scroll';
const scrollAmount = 3333333; const scrollAmount = 3333333;
const getElmDirectionIsRTL = (elm: HTMLElement): boolean => style(elm, 'direction') === 'rtl'; const getElmDirectionIsRTL = (elm: HTMLElement): boolean => style(elm, 'direction') === 'rtl';
const domRectHasDimensions = (rect?: DOMRectReadOnly) => rect && (rect.height || rect.width); const domRectHasDimensions = (rect?: DOMRectReadOnly) => rect && (rect.height || rect.width);
@@ -176,7 +174,7 @@ export const createSizeObserver = (
push( push(
offListeners, offListeners,
on(sizeObserver, scrollEventName, (event: Event) => { on(sizeObserver, 'scroll', (event: Event) => {
const directionIsRTLCacheValues = updateDirectionIsRTLCache(); const directionIsRTLCacheValues = updateDirectionIsRTLCache();
const [directionIsRTL, directionIsRTLChanged] = directionIsRTLCacheValues; const [directionIsRTL, directionIsRTLChanged] = directionIsRTLCacheValues;
@@ -200,7 +198,7 @@ export const createSizeObserver = (
addClass(sizeObserver, classNameSizeObserverAppear); addClass(sizeObserver, classNameSizeObserverAppear);
push( push(
offListeners, offListeners,
on(sizeObserver, animationStartEventName, appearCallback, { on(sizeObserver, 'animationstart', appearCallback, {
// Fire only once for "CSS is ready" event if ResizeObserver strategy is used // Fire only once for "CSS is ready" event if ResizeObserver strategy is used
_once: !!ResizeObserverConstructor, _once: !!ResizeObserverConstructor,
}) })
@@ -13,7 +13,11 @@ import {
import { createSizeObserver } from 'observers/sizeObserver'; import { createSizeObserver } from 'observers/sizeObserver';
import { classNameTrinsicObserver } from 'classnames'; import { classNameTrinsicObserver } from 'classnames';
export type DestroyTrinsicObserver = () => void; export type TrinsicObserverCallback = (heightIntrinsic: CacheValues<boolean>) => any;
export type TrinsicObserver = [
destroy: () => void,
update: () => void | Parameters<TrinsicObserverCallback>
];
const isHeightIntrinsic = (ioEntryOrSize: IntersectionObserverEntry | WH<number>): boolean => const isHeightIntrinsic = (ioEntryOrSize: IntersectionObserverEntry | WH<number>): boolean =>
(ioEntryOrSize as WH<number>).h === 0 || (ioEntryOrSize as WH<number>).h === 0 ||
@@ -28,39 +32,45 @@ const isHeightIntrinsic = (ioEntryOrSize: IntersectionObserverEntry | WH<number>
*/ */
export const createTrinsicObserver = ( export const createTrinsicObserver = (
target: HTMLElement, target: HTMLElement,
onTrinsicChangedCallback: (heightIntrinsic: CacheValues<boolean>) => any onTrinsicChangedCallback: TrinsicObserverCallback
): DestroyTrinsicObserver => { ): TrinsicObserver => {
let intersectionObserverInstance: undefined | IntersectionObserver;
const trinsicObserver = createDiv(classNameTrinsicObserver); const trinsicObserver = createDiv(classNameTrinsicObserver);
const offListeners: (() => void)[] = []; const offListeners: (() => void)[] = [];
const [updateHeightIntrinsicCache] = createCache({ const [updateHeightIntrinsicCache] = createCache({
_initialValue: false, _initialValue: false,
}); });
const triggerOnTrinsicChangedCallback = ( const triggerOnTrinsicChangedCallback = (
updateValue?: IntersectionObserverEntry | WH<number> updateValue?: IntersectionObserverEntry | WH<number>,
) => { fromRecords?: true
): void | Parameters<TrinsicObserverCallback> => {
if (updateValue) { if (updateValue) {
const heightIntrinsic = updateHeightIntrinsicCache(isHeightIntrinsic(updateValue)); const heightIntrinsic = updateHeightIntrinsicCache(isHeightIntrinsic(updateValue));
const [, heightIntrinsicChanged] = heightIntrinsic; const [, heightIntrinsicChanged] = heightIntrinsic;
if (heightIntrinsicChanged) { if (heightIntrinsicChanged) {
onTrinsicChangedCallback(heightIntrinsic); !fromRecords && onTrinsicChangedCallback(heightIntrinsic);
return [heightIntrinsic];
} }
} }
}; };
const intersectionObserverCallback = (
entries: IntersectionObserverEntry[],
fromRecords?: true
) => {
if (entries && entries.length > 0) {
return triggerOnTrinsicChangedCallback(entries.pop(), fromRecords);
}
};
if (IntersectionObserverConstructor) { if (IntersectionObserverConstructor) {
const intersectionObserverInstance: IntersectionObserver = new IntersectionObserverConstructor( intersectionObserverInstance = new IntersectionObserverConstructor(
(entries: IntersectionObserverEntry[]) => { (entries) => intersectionObserverCallback(entries),
if (entries && entries.length > 0) {
triggerOnTrinsicChangedCallback(entries.pop());
}
},
{ root: target } { root: target }
); );
intersectionObserverInstance.observe(trinsicObserver); intersectionObserverInstance.observe(trinsicObserver);
push(offListeners, () => { push(offListeners, () => {
intersectionObserverInstance.disconnect(); intersectionObserverInstance!.disconnect();
}); });
} else { } else {
const onSizeChanged = () => { const onSizeChanged = () => {
@@ -73,8 +83,15 @@ export const createTrinsicObserver = (
prependChildren(target, trinsicObserver); prependChildren(target, trinsicObserver);
return () => { return [
runEachAndClear(offListeners); () => {
removeElements(trinsicObserver); runEachAndClear(offListeners);
}; removeElements(trinsicObserver);
},
() => {
if (intersectionObserverInstance) {
return intersectionObserverCallback(intersectionObserverInstance.takeRecords(), true);
}
},
];
}; };
@@ -18,6 +18,8 @@ import {
isFunction, isFunction,
ResizeObserverConstructor, ResizeObserverConstructor,
closest, closest,
assignDeep,
push,
} from 'support'; } from 'support';
import { getEnvironment } from 'environment'; import { getEnvironment } from 'environment';
import { import {
@@ -41,8 +43,9 @@ import type {
export type StructureSetupObserversUpdate = (checkOption: SetupUpdateCheckOption) => void; export type StructureSetupObserversUpdate = (checkOption: SetupUpdateCheckOption) => void;
export type StructureSetupObservers = [ export type StructureSetupObservers = [
updateObserverOptions: StructureSetupObserversUpdate, destroy: () => void,
destroy: () => void updateObservers: () => Partial<StructureSetupUpdateHints>,
updateObserversOptions: StructureSetupObserversUpdate
]; ];
type ExcludeFromTuple<T extends readonly any[], E> = T extends [infer F, ...infer R] type ExcludeFromTuple<T extends readonly any[], E> = T extends [infer F, ...infer R]
@@ -69,7 +72,7 @@ export const createStructureSetupObservers = (
): StructureSetupObservers => { ): StructureSetupObservers => {
let debounceTimeout: number | false | undefined; let debounceTimeout: number | false | undefined;
let debounceMaxDelay: number | false | undefined; let debounceMaxDelay: number | false | undefined;
let contentMutationObserver: DOMObserver | undefined; let contentMutationObserver: DOMObserver<true> | undefined;
const [, setState] = state; const [, setState] = state;
const { const {
_host, _host,
@@ -134,10 +137,14 @@ export const createStructureSetupObservers = (
} }
}); });
}; };
const onTrinsicChanged = (heightIntrinsicCache: CacheValues<boolean>) => { const onTrinsicChanged = (heightIntrinsicCache: CacheValues<boolean>, fromRecords?: true) => {
const [heightIntrinsic, heightIntrinsicChanged] = heightIntrinsicCache; const [heightIntrinsic, heightIntrinsicChanged] = heightIntrinsicCache;
const updateHints: Partial<StructureSetupUpdateHints> = {
_heightIntrinsicChanged: heightIntrinsicChanged,
};
setState({ _heightIntrinsic: heightIntrinsic }); setState({ _heightIntrinsic: heightIntrinsic });
structureSetupUpdate({ _heightIntrinsicChanged: heightIntrinsicChanged }); !fromRecords && structureSetupUpdate(updateHints);
return updateHints;
}; };
const onSizeChanged = ({ const onSizeChanged = ({
_sizeChanged, _sizeChanged,
@@ -158,30 +165,36 @@ export const createStructureSetupObservers = (
updateFn({ _sizeChanged, _directionChanged: directionChanged }); updateFn({ _sizeChanged, _directionChanged: directionChanged });
}; };
const onContentMutation = (contentChangedTroughEvent: boolean) => { const onContentMutation = (contentChangedTroughEvent: boolean, fromRecords?: true) => {
const [, contentSizeChanged] = updateContentSizeCache(); const [, contentSizeChanged] = updateContentSizeCache();
const updateHints: Partial<StructureSetupUpdateHints> = {
_contentMutation: contentSizeChanged,
};
// if contentChangedTroughEvent is true its already debounced // if contentChangedTroughEvent is true its already debounced
const updateFn = contentChangedTroughEvent const updateFn = contentChangedTroughEvent
? structureSetupUpdate ? structureSetupUpdate
: structureSetupUpdateWithDebouncedAdaptiveUpdateHints; : structureSetupUpdateWithDebouncedAdaptiveUpdateHints;
if (contentSizeChanged) { if (contentSizeChanged) {
updateFn({ !fromRecords && updateFn(updateHints);
_contentMutation: true,
});
} }
return updateHints;
}; };
const onHostMutation = (targetChangedAttrs: string[], targetStyleChanged: boolean) => { const onHostMutation = (
targetChangedAttrs: string[],
targetStyleChanged: boolean,
fromRecords?: true
) => {
const updateHints: Partial<StructureSetupUpdateHints> = { _hostMutation: targetStyleChanged };
if (targetStyleChanged) { if (targetStyleChanged) {
structureSetupUpdateWithDebouncedAdaptiveUpdateHints({ !fromRecords && structureSetupUpdateWithDebouncedAdaptiveUpdateHints(updateHints);
_hostMutation: true,
});
} else if (!_viewportIsTarget) { } else if (!_viewportIsTarget) {
updateViewportAttrsFromHost(targetChangedAttrs); updateViewportAttrsFromHost(targetChangedAttrs);
} }
return updateHints;
}; };
const destroyTrinsicObserver = const trinsicObserver =
(_content || !_flexboxGlue) && createTrinsicObserver(_host, onTrinsicChanged); (_content || !_flexboxGlue) && createTrinsicObserver(_host, onTrinsicChanged);
const destroySizeObserver = const destroySizeObserver =
!_viewportIsTarget && !_viewportIsTarget &&
@@ -189,10 +202,15 @@ export const createStructureSetupObservers = (
_appear: true, _appear: true,
_direction: !_nativeScrollbarStyling, _direction: !_nativeScrollbarStyling,
}); });
const [destroyHostMutationObserver] = createDOMObserver(_host, false, onHostMutation, { const [destroyHostMutationObserver, updateHostMutationObserver] = createDOMObserver(
_styleChangingAttributes: baseStyleChangingAttrs, _host,
_attributes: baseStyleChangingAttrs.concat(viewportAttrsFromTarget), false,
}); onHostMutation,
{
_styleChangingAttributes: baseStyleChangingAttrs,
_attributes: baseStyleChangingAttrs.concat(viewportAttrsFromTarget),
}
);
const viewportIsTargetResizeObserver = const viewportIsTargetResizeObserver =
_viewportIsTarget && _viewportIsTarget &&
@@ -202,6 +220,58 @@ export const createStructureSetupObservers = (
updateViewportAttrsFromHost(); updateViewportAttrsFromHost();
return [ return [
() => {
contentMutationObserver && contentMutationObserver[0](); // destroy
trinsicObserver && trinsicObserver[0](); // destroy
destroySizeObserver && destroySizeObserver();
viewportIsTargetResizeObserver && viewportIsTargetResizeObserver.disconnect();
destroyHostMutationObserver();
},
() => {
const updateHints: Partial<StructureSetupUpdateHints> = {};
const hostUpdateResult = updateHostMutationObserver();
const contentUpdateResult = contentMutationObserver && contentMutationObserver[1](); // update
const trinsicUpdateResult = trinsicObserver && trinsicObserver[1](); // update
if (hostUpdateResult) {
assignDeep(
updateHints,
onHostMutation.apply(
0,
push(hostUpdateResult, true) as [
...updateResult: typeof hostUpdateResult,
fromRecords: true
]
)
);
}
if (contentUpdateResult) {
assignDeep(
updateHints,
onContentMutation.apply(
0,
push(contentUpdateResult, true) as [
...updateResult: typeof contentUpdateResult,
fromRecords: true
]
)
);
}
if (trinsicUpdateResult) {
assignDeep(
updateHints,
onTrinsicChanged.apply(
0,
push(trinsicUpdateResult as any[], true) as [
...updateResult: typeof trinsicUpdateResult,
fromRecords: true
]
)
);
}
return updateHints;
},
(checkOption) => { (checkOption) => {
const [ignoreMutation] = checkOption<string[] | null>('updating.ignoreMutation'); const [ignoreMutation] = checkOption<string[] | null>('updating.ignoreMutation');
const [attributes, attributesChanged] = checkOption<string[] | null>('updating.attributes'); const [attributes, attributesChanged] = checkOption<string[] | null>('updating.attributes');
@@ -261,12 +331,5 @@ export const createStructureSetupObservers = (
} }
} }
}, },
() => {
contentMutationObserver && contentMutationObserver[0](); // destroy
destroyTrinsicObserver && destroyTrinsicObserver();
destroySizeObserver && destroySizeObserver();
viewportIsTargetResizeObserver && viewportIsTargetResizeObserver.disconnect();
destroyHostMutationObserver();
},
]; ];
}; };
@@ -1,4 +1,4 @@
import { createEventListenerHub } from 'support'; import { createEventListenerHub, isEmptyObject, keys } from 'support';
import { createState, createOptionCheck } from 'setups/setups'; import { createState, createOptionCheck } from 'setups/setups';
import { createStructureSetupElements } from 'setups/structureSetup/structureSetup.elements'; import { createStructureSetupElements } from 'setups/structureSetup/structureSetup.elements';
import { createStructureSetupUpdate } from 'setups/structureSetup/structureSetup.update'; import { createStructureSetupUpdate } from 'setups/structureSetup/structureSetup.update';
@@ -79,11 +79,22 @@ export const createStructureSetup = (
const [getState] = state; const [getState] = state;
const [elements, appendElements, destroyElements] = createStructureSetupElements(target); const [elements, appendElements, destroyElements] = createStructureSetupElements(target);
const updateStructure = createStructureSetupUpdate(elements, state); const updateStructure = createStructureSetupUpdate(elements, state);
const [updateObservers, destroyObservers] = createStructureSetupObservers( const triggerUpdateEvent: (...args: StructureSetupEventMap['u']) => void = (
updateHints,
changedOptions,
force
) => {
const truthyUpdateHints = keys(updateHints).some((key) => updateHints[key]);
if (truthyUpdateHints || !isEmptyObject(changedOptions) || force) {
triggerEvent('u', [updateHints, changedOptions, force]);
}
};
const [destroyObservers, updateObservers, updateObserversOptions] = createStructureSetupObservers(
elements, elements,
state, state,
(updateHints) => { (updateHints) => {
triggerEvent('u', [updateStructure(checkOptionsFallback, updateHints), {}, false]); triggerUpdateEvent(updateStructure(checkOptionsFallback, updateHints), {}, false);
} }
); );
@@ -98,8 +109,12 @@ export const createStructureSetup = (
return [ return [
(changedOptions, force?) => { (changedOptions, force?) => {
const checkOption = createOptionCheck(options, changedOptions, force); const checkOption = createOptionCheck(options, changedOptions, force);
updateObservers(checkOption); updateObserversOptions(checkOption);
triggerEvent('u', [updateStructure(checkOption, {}, force), changedOptions, !!force]); triggerUpdateEvent(
updateStructure(checkOption, updateObservers(), force),
changedOptions,
!!force
);
}, },
structureSetupState, structureSetupState,
() => { () => {
@@ -29,7 +29,7 @@ const startBtn: HTMLButtonElement | null = document.querySelector('#start');
const changesSlot: HTMLButtonElement | null = document.querySelector('#changes'); const changesSlot: HTMLButtonElement | null = document.querySelector('#changes');
const preInitChildren = targetElm?.children.length; const preInitChildren = targetElm?.children.length;
const destroyTrinsicObserver = createTrinsicObserver( const [destroyTrinsicObserver, updateTrinsicObserver] = createTrinsicObserver(
targetElm as HTMLElement, targetElm as HTMLElement,
(heightIntrinsicCache) => { (heightIntrinsicCache) => {
const [currentHeightIntrinsic, currentHeightIntrinsicChanged] = heightIntrinsicCache; const [currentHeightIntrinsic, currentHeightIntrinsicChanged] = heightIntrinsicCache;
@@ -158,6 +158,9 @@ const start = async () => {
}); });
await changeWhileHidden(); await changeWhileHidden();
updateTrinsicObserver();
destroyTrinsicObserver();
updateTrinsicObserver();
destroyTrinsicObserver(); destroyTrinsicObserver();
should.equal( should.equal(
targetElm?.children.length, targetElm?.children.length,