mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-05-30 05:24:06 +03:00
357 lines
12 KiB
TypeScript
357 lines
12 KiB
TypeScript
import {
|
|
debounce,
|
|
isArray,
|
|
isNumber,
|
|
each,
|
|
indexOf,
|
|
isString,
|
|
attr,
|
|
removeAttr,
|
|
CacheValues,
|
|
keys,
|
|
liesBetween,
|
|
scrollSize,
|
|
equalWH,
|
|
createCache,
|
|
WH,
|
|
fractionalSize,
|
|
isFunction,
|
|
ResizeObserverConstructor,
|
|
closest,
|
|
assignDeep,
|
|
push,
|
|
scrollLeft,
|
|
scrollTop,
|
|
noop,
|
|
} from 'support';
|
|
import { getEnvironment } from 'environment';
|
|
import {
|
|
dataAttributeHost,
|
|
dataValueHostOverflowVisible,
|
|
dataValueHostUpdating,
|
|
classNameViewport,
|
|
classNameOverflowVisible,
|
|
classNameScrollbar,
|
|
classNameViewportArrange,
|
|
} from 'classnames';
|
|
import { createSizeObserver, SizeObserverCallbackParams } from 'observers/sizeObserver';
|
|
import { createTrinsicObserver } from 'observers/trinsicObserver';
|
|
import { createDOMObserver, DOMObserver } from 'observers/domObserver';
|
|
import type { SetupState, SetupUpdateCheckOption } from 'setups';
|
|
import type { StructureSetupState } from 'setups/structureSetup';
|
|
import type { StructureSetupElementsObj } from 'setups/structureSetup/structureSetup.elements';
|
|
import type {
|
|
StructureSetupUpdate,
|
|
StructureSetupUpdateHints,
|
|
} from 'setups/structureSetup/structureSetup.update';
|
|
|
|
export type StructureSetupObserversUpdate = (checkOption: SetupUpdateCheckOption) => void;
|
|
|
|
export type StructureSetupObservers = [
|
|
destroy: () => void,
|
|
appendElements: () => void,
|
|
updateObservers: () => Partial<StructureSetupUpdateHints>,
|
|
updateObserversOptions: StructureSetupObserversUpdate
|
|
];
|
|
|
|
type ExcludeFromTuple<T extends readonly any[], E> = T extends [infer F, ...infer R]
|
|
? [F] extends [E]
|
|
? ExcludeFromTuple<R, E>
|
|
: [F, ...ExcludeFromTuple<R, E>]
|
|
: [];
|
|
|
|
const hostSelector = `[${dataAttributeHost}]`;
|
|
|
|
// TODO: observer textarea attrs if textarea
|
|
|
|
const viewportSelector = `.${classNameViewport}`;
|
|
const viewportAttrsFromTarget = ['tabindex'];
|
|
const baseStyleChangingAttrsTextarea = ['wrap', 'cols', 'rows'];
|
|
const baseStyleChangingAttrs = ['id', 'class', 'style', 'open'];
|
|
|
|
export const createStructureSetupObservers = (
|
|
structureSetupElements: StructureSetupElementsObj,
|
|
state: SetupState<StructureSetupState>,
|
|
structureSetupUpdate: (
|
|
...args: ExcludeFromTuple<Parameters<StructureSetupUpdate>, Parameters<StructureSetupUpdate>[0]>
|
|
) => any
|
|
): StructureSetupObservers => {
|
|
let debounceTimeout: number | false | undefined;
|
|
let debounceMaxDelay: number | false | undefined;
|
|
let contentMutationObserver: DOMObserver<true> | undefined;
|
|
const [, setState] = state;
|
|
const {
|
|
_host,
|
|
_viewport,
|
|
_content,
|
|
_isTextarea,
|
|
_viewportIsTarget,
|
|
_viewportHasClass,
|
|
_viewportAddRemoveClass,
|
|
} = structureSetupElements;
|
|
const { _flexboxGlue } = getEnvironment();
|
|
|
|
const [updateContentSizeCache] = createCache<WH<number>>(
|
|
{
|
|
_equal: equalWH,
|
|
_initialValue: { w: 0, h: 0 },
|
|
},
|
|
() => {
|
|
const hasOver = _viewportHasClass(classNameOverflowVisible, dataValueHostOverflowVisible);
|
|
const hasVpStyle = _viewportHasClass(classNameViewportArrange, '');
|
|
const scrollOffsetX = hasVpStyle && scrollLeft(_viewport);
|
|
const scrollOffsetY = hasVpStyle && scrollTop(_viewport);
|
|
_viewportAddRemoveClass(classNameOverflowVisible, dataValueHostOverflowVisible);
|
|
_viewportAddRemoveClass(classNameViewportArrange, '');
|
|
_viewportAddRemoveClass('', dataValueHostUpdating, true);
|
|
|
|
const contentScroll = scrollSize(_content);
|
|
const viewportScroll = scrollSize(_viewport);
|
|
const fractional = fractionalSize(_viewport);
|
|
|
|
_viewportAddRemoveClass(classNameOverflowVisible, dataValueHostOverflowVisible, hasOver);
|
|
_viewportAddRemoveClass(classNameViewportArrange, '', hasVpStyle);
|
|
_viewportAddRemoveClass('', dataValueHostUpdating);
|
|
scrollLeft(_viewport, scrollOffsetX);
|
|
scrollTop(_viewport, scrollOffsetY);
|
|
return {
|
|
w: viewportScroll.w + contentScroll.w + fractional.w,
|
|
h: viewportScroll.h + contentScroll.h + fractional.h,
|
|
};
|
|
}
|
|
);
|
|
const contentMutationObserverAttr = _isTextarea
|
|
? baseStyleChangingAttrsTextarea
|
|
: baseStyleChangingAttrs.concat(baseStyleChangingAttrsTextarea);
|
|
const structureSetupUpdateWithDebouncedAdaptiveUpdateHints = debounce(structureSetupUpdate, {
|
|
_timeout: () => debounceTimeout,
|
|
_maxDelay: () => debounceMaxDelay,
|
|
_mergeParams(prev, curr) {
|
|
const [prevObj] = prev;
|
|
const [currObj] = curr;
|
|
return [
|
|
keys(prevObj)
|
|
.concat(keys(currObj))
|
|
.reduce((obj, key) => {
|
|
obj[key] = prevObj[key] || currObj[key];
|
|
return obj;
|
|
}, {}),
|
|
] as [Partial<StructureSetupUpdateHints>];
|
|
},
|
|
});
|
|
|
|
const updateViewportAttrsFromHost = (attributes?: string[]) => {
|
|
each(attributes || viewportAttrsFromTarget, (attribute) => {
|
|
if (indexOf(viewportAttrsFromTarget, attribute) > -1) {
|
|
const hostAttr = attr(_host, attribute);
|
|
if (isString(hostAttr)) {
|
|
attr(_viewport, attribute, hostAttr);
|
|
} else {
|
|
removeAttr(_viewport, attribute);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
const onTrinsicChanged = (heightIntrinsicCache: CacheValues<boolean>, fromRecords?: true) => {
|
|
const [heightIntrinsic, heightIntrinsicChanged] = heightIntrinsicCache;
|
|
const updateHints: Partial<StructureSetupUpdateHints> = {
|
|
_heightIntrinsicChanged: heightIntrinsicChanged,
|
|
};
|
|
setState({ _heightIntrinsic: heightIntrinsic });
|
|
!fromRecords && structureSetupUpdate(updateHints);
|
|
return updateHints;
|
|
};
|
|
const onSizeChanged = ({
|
|
_sizeChanged,
|
|
_directionIsRTLCache,
|
|
_appear,
|
|
}: SizeObserverCallbackParams) => {
|
|
const updateFn =
|
|
!_sizeChanged || _appear
|
|
? structureSetupUpdate
|
|
: structureSetupUpdateWithDebouncedAdaptiveUpdateHints;
|
|
|
|
let directionChanged = false;
|
|
if (_directionIsRTLCache) {
|
|
const [directionIsRTL, directionIsRTLChanged] = _directionIsRTLCache;
|
|
directionChanged = directionIsRTLChanged;
|
|
setState({ _directionIsRTL: directionIsRTL });
|
|
}
|
|
|
|
updateFn({ _sizeChanged, _directionChanged: directionChanged });
|
|
};
|
|
const onContentMutation = (contentChangedTroughEvent: boolean, fromRecords?: true) => {
|
|
const [, contentSizeChanged] = updateContentSizeCache();
|
|
const updateHints: Partial<StructureSetupUpdateHints> = {
|
|
_contentMutation: contentSizeChanged,
|
|
};
|
|
// if contentChangedTroughEvent is true its already debounced
|
|
const updateFn = contentChangedTroughEvent
|
|
? structureSetupUpdate
|
|
: structureSetupUpdateWithDebouncedAdaptiveUpdateHints;
|
|
|
|
if (contentSizeChanged) {
|
|
!fromRecords && updateFn(updateHints);
|
|
}
|
|
return updateHints;
|
|
};
|
|
const onHostMutation = (
|
|
targetChangedAttrs: string[],
|
|
targetStyleChanged: boolean,
|
|
fromRecords?: true
|
|
) => {
|
|
const updateHints: Partial<StructureSetupUpdateHints> = { _hostMutation: targetStyleChanged };
|
|
if (targetStyleChanged) {
|
|
!fromRecords && structureSetupUpdateWithDebouncedAdaptiveUpdateHints(updateHints);
|
|
} else if (!_viewportIsTarget) {
|
|
updateViewportAttrsFromHost(targetChangedAttrs);
|
|
}
|
|
return updateHints;
|
|
};
|
|
|
|
const [destroyTrinsicObserver, appendTrinsicObserver, updateTrinsicObserver] =
|
|
_content || !_flexboxGlue ? createTrinsicObserver(_host, onTrinsicChanged) : [noop, noop, noop];
|
|
const [destroySizeObserver, appendSizeObserver] = !_viewportIsTarget
|
|
? createSizeObserver(_host, onSizeChanged, {
|
|
_appear: true,
|
|
_direction: true,
|
|
})
|
|
: [noop, noop];
|
|
const [destroyHostMutationObserver, updateHostMutationObserver] = createDOMObserver(
|
|
_host,
|
|
false,
|
|
onHostMutation,
|
|
{
|
|
_styleChangingAttributes: baseStyleChangingAttrs,
|
|
_attributes: baseStyleChangingAttrs.concat(viewportAttrsFromTarget),
|
|
}
|
|
);
|
|
|
|
const viewportIsTargetResizeObserver =
|
|
_viewportIsTarget &&
|
|
ResizeObserverConstructor &&
|
|
new ResizeObserverConstructor(onSizeChanged.bind(0, { _sizeChanged: true }));
|
|
|
|
viewportIsTargetResizeObserver && viewportIsTargetResizeObserver.observe(_host);
|
|
updateViewportAttrsFromHost();
|
|
|
|
return [
|
|
() => {
|
|
destroyTrinsicObserver();
|
|
destroySizeObserver();
|
|
contentMutationObserver && contentMutationObserver[0](); // destroy
|
|
viewportIsTargetResizeObserver && viewportIsTargetResizeObserver.disconnect();
|
|
destroyHostMutationObserver();
|
|
},
|
|
() => {
|
|
// order is matter!
|
|
appendSizeObserver();
|
|
appendTrinsicObserver();
|
|
},
|
|
() => {
|
|
const updateHints: Partial<StructureSetupUpdateHints> = {};
|
|
const hostUpdateResult = updateHostMutationObserver();
|
|
const trinsicUpdateResult = updateTrinsicObserver();
|
|
const contentUpdateResult = contentMutationObserver && contentMutationObserver[1](); // update
|
|
|
|
if (hostUpdateResult) {
|
|
assignDeep(
|
|
updateHints,
|
|
onHostMutation.apply(
|
|
0,
|
|
push(hostUpdateResult, true) as [
|
|
...updateResult: typeof hostUpdateResult,
|
|
fromRecords: true
|
|
]
|
|
)
|
|
);
|
|
}
|
|
if (trinsicUpdateResult) {
|
|
assignDeep(
|
|
updateHints,
|
|
onTrinsicChanged.apply(
|
|
0,
|
|
push(trinsicUpdateResult as any[], true) as [
|
|
...updateResult: typeof trinsicUpdateResult,
|
|
fromRecords: true
|
|
]
|
|
)
|
|
);
|
|
}
|
|
if (contentUpdateResult) {
|
|
assignDeep(
|
|
updateHints,
|
|
onContentMutation.apply(
|
|
0,
|
|
push(contentUpdateResult, true) as [
|
|
...updateResult: typeof contentUpdateResult,
|
|
fromRecords: true
|
|
]
|
|
)
|
|
);
|
|
}
|
|
|
|
return updateHints;
|
|
},
|
|
(checkOption) => {
|
|
const [ignoreMutation] = checkOption<string[] | null>('updating.ignoreMutation');
|
|
const [attributes, attributesChanged] = checkOption<string[] | null>('updating.attributes');
|
|
const [elementEvents, elementEventsChanged] = checkOption<Array<[string, string]> | null>(
|
|
'updating.elementEvents'
|
|
);
|
|
const [debounceValue, debounceChanged] = checkOption<Array<number> | number | null>(
|
|
'updating.debounce'
|
|
);
|
|
const updateContentMutationObserver = elementEventsChanged || attributesChanged;
|
|
const ignoreMutationFromOptions = (mutation: MutationRecord) =>
|
|
isFunction(ignoreMutation) && ignoreMutation(mutation);
|
|
|
|
if (updateContentMutationObserver) {
|
|
if (contentMutationObserver) {
|
|
contentMutationObserver[1](); // update
|
|
contentMutationObserver[0](); // destroy
|
|
}
|
|
contentMutationObserver = createDOMObserver(
|
|
_content || _viewport,
|
|
true,
|
|
onContentMutation,
|
|
{
|
|
_styleChangingAttributes: contentMutationObserverAttr.concat(attributes || []),
|
|
_attributes: contentMutationObserverAttr.concat(attributes || []),
|
|
_eventContentChange: elementEvents,
|
|
_nestedTargetSelector: hostSelector,
|
|
_ignoreContentChange: (mutation, isNestedTarget) => {
|
|
const { target, attributeName } = mutation;
|
|
const ignore =
|
|
!isNestedTarget && attributeName
|
|
? liesBetween(target, hostSelector, viewportSelector)
|
|
: false;
|
|
return (
|
|
ignore ||
|
|
!!closest(target, `.${classNameScrollbar}`) || // ignore explicitely all scrollbar elements
|
|
!!ignoreMutationFromOptions(mutation)
|
|
);
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
if (debounceChanged) {
|
|
structureSetupUpdateWithDebouncedAdaptiveUpdateHints._flush();
|
|
if (isArray(debounceValue)) {
|
|
const timeout = debounceValue[0];
|
|
const maxWait = debounceValue[1];
|
|
debounceTimeout = isNumber(timeout) ? timeout : false;
|
|
debounceMaxDelay = isNumber(maxWait) ? maxWait : false;
|
|
} else if (isNumber(debounceValue)) {
|
|
debounceTimeout = debounceValue;
|
|
debounceMaxDelay = false;
|
|
} else {
|
|
debounceTimeout = false;
|
|
debounceMaxDelay = false;
|
|
}
|
|
}
|
|
},
|
|
];
|
|
};
|