mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-05-21 17:54:09 +03:00
add structureSetup
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
hasOwnProperty,
|
||||
isEmptyObject,
|
||||
} from 'support';
|
||||
import { CSSDirection, PlainObject } from 'typings';
|
||||
import { PlainObject } from 'typings';
|
||||
|
||||
interface LifecycleBaseUpdateHints<O> {
|
||||
_force?: boolean;
|
||||
@@ -23,7 +23,7 @@ export interface LifecycleBase<O extends PlainObject> {
|
||||
export interface Lifecycle<T extends PlainObject> extends LifecycleBase<T> {
|
||||
_destruct(): void;
|
||||
_onSizeChanged?(): void;
|
||||
_onDirectionChanged?(directionCache: Cache<CSSDirection>): void;
|
||||
_onDirectionChanged?(directionCache: Cache<boolean>): void;
|
||||
_onTrinsicChanged?(widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
scrollSize,
|
||||
offsetSize,
|
||||
} from 'support';
|
||||
import { OSTargetObject } from 'typings';
|
||||
import { PreparedOSTargetObject } from 'setups/structureSetup';
|
||||
import { createLifecycleBase, Lifecycle } from 'lifecycles/lifecycleBase';
|
||||
import { getEnvironment, Environment } from 'environment';
|
||||
|
||||
@@ -42,10 +42,10 @@ const cssMarginEnd = cssProperty('margin-inline-end');
|
||||
const cssBorderEnd = cssProperty('border-inline-end');
|
||||
|
||||
export const createStructureLifecycle = (
|
||||
target: OSTargetObject,
|
||||
target: PreparedOSTargetObject,
|
||||
initialOptions?: StructureLifecycleOptions
|
||||
): Lifecycle<StructureLifecycleOptions> => {
|
||||
const { host, padding: paddingElm, viewport, content } = target;
|
||||
const { _host, _padding, _viewport, _content } = target;
|
||||
const destructFns: (() => any)[] = [];
|
||||
const env: Environment = getEnvironment();
|
||||
const scrollbarsOverlaid = env._nativeScrollbarIsOverlaid;
|
||||
@@ -54,7 +54,7 @@ export const createStructureLifecycle = (
|
||||
// direction change is only needed to update scrollbar hiding, therefore its not needed if css can do it, scrollbars are invisible or overlaid on y axis
|
||||
const directionObserverObsolete = (cssMarginEnd && cssBorderEnd) || supportsScrollbarStyling || scrollbarsOverlaid.y;
|
||||
|
||||
const updatePaddingCache = createCache(() => topRightBottomLeft(host, 'padding'), { _equal: equalTRBL });
|
||||
const updatePaddingCache = createCache(() => topRightBottomLeft(_host, 'padding'), { _equal: equalTRBL });
|
||||
const updateOverflowAmountCache = createCache<XY<number>, { _contentScrollSize: WH<number>; _viewportSize: WH<number> }>(
|
||||
(ctx) => ({
|
||||
x: Math.max(0, Math.round((ctx!._contentScrollSize.w - ctx!._viewportSize.w) * 100) / 100),
|
||||
@@ -82,7 +82,7 @@ export const createStructureLifecycle = (
|
||||
paddingStyle.l = -padding!.l;
|
||||
}
|
||||
|
||||
style(paddingElm, {
|
||||
style(_padding, {
|
||||
top: paddingStyle.t,
|
||||
left: paddingStyle.l,
|
||||
'margin-right': paddingStyle.r,
|
||||
@@ -91,9 +91,9 @@ export const createStructureLifecycle = (
|
||||
});
|
||||
}
|
||||
|
||||
const viewportOffsetSize = offsetSize(paddingElm);
|
||||
const contentClientSize = offsetSize(content);
|
||||
const contentScrollSize = scrollSize(content);
|
||||
const viewportOffsetSize = offsetSize(_padding);
|
||||
const contentClientSize = offsetSize(_content);
|
||||
const contentScrollSize = scrollSize(_content);
|
||||
const overflowAmuntCache = updateOverflowAmountCache(force, {
|
||||
_contentScrollSize: contentScrollSize,
|
||||
_viewportSize: {
|
||||
@@ -151,7 +151,7 @@ export const createStructureLifecycle = (
|
||||
const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>) => {
|
||||
const { _changed, _value } = heightIntrinsicCache;
|
||||
if (_changed) {
|
||||
style(content, { height: _value ? 'auto' : '100%' });
|
||||
style(_content, { height: _value ? 'auto' : '100%' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,54 +1,21 @@
|
||||
import { OSTarget, OSTargetObject, CSSDirection } from 'typings';
|
||||
import { OSTarget, OSTargetObject } from 'typings';
|
||||
import { createStructureLifecycle } from 'lifecycles/structureLifecycle';
|
||||
import { Cache, appendChildren, addClass, contents, is, isHTMLElement, createDiv, each, push } from 'support';
|
||||
import { Cache, each, push } from 'support';
|
||||
import { createSizeObserver } from 'observers/sizeObserver';
|
||||
import { createTrinsicObserver } from 'observers/trinsicObserver';
|
||||
import { createDOMObserver } from 'observers/domObserver';
|
||||
import { createStructureSetup, StructureSetup } from 'setups/structureSetup';
|
||||
import { Lifecycle } from 'lifecycles/lifecycleBase';
|
||||
import { classNameHost, classNamePadding, classNameViewport, classNameContent } from 'classnames';
|
||||
|
||||
const normalizeTarget = (target: OSTarget): OSTargetObject => {
|
||||
if (isHTMLElement(target)) {
|
||||
const isTextarea = is(target, 'textarea');
|
||||
const host = (isTextarea ? createDiv() : target) as HTMLElement;
|
||||
const padding = createDiv(classNamePadding);
|
||||
const viewport = createDiv(classNameViewport);
|
||||
const content = createDiv(classNameContent);
|
||||
|
||||
appendChildren(padding, viewport);
|
||||
appendChildren(viewport, content);
|
||||
appendChildren(content, contents(target));
|
||||
appendChildren(target, padding);
|
||||
addClass(host, classNameHost);
|
||||
|
||||
return {
|
||||
target,
|
||||
host,
|
||||
padding,
|
||||
viewport,
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
const { host, padding, viewport, content } = target;
|
||||
|
||||
addClass(host, classNameHost);
|
||||
addClass(padding, classNamePadding);
|
||||
addClass(viewport, classNameViewport);
|
||||
addClass(content, classNameContent);
|
||||
|
||||
return target;
|
||||
};
|
||||
|
||||
const OverlayScrollbars = (target: OSTarget, options?: any, extensions?: any): void => {
|
||||
const osTarget: OSTargetObject = normalizeTarget(target);
|
||||
const OverlayScrollbars = (target: OSTarget | OSTargetObject, options?: any, extensions?: any): void => {
|
||||
const structureSetup: StructureSetup = createStructureSetup(target);
|
||||
const lifecycles: Lifecycle<any>[] = [];
|
||||
const { host, content } = osTarget;
|
||||
const { _host, _viewport, _content } = structureSetup._targetObj;
|
||||
|
||||
push(lifecycles, createStructureLifecycle(osTarget));
|
||||
push(lifecycles, createStructureLifecycle(structureSetup._targetObj));
|
||||
|
||||
// eslint-disable-next-line
|
||||
const onSizeChanged = (directionCache?: Cache<CSSDirection>) => {
|
||||
const onSizeChanged = (directionCache?: Cache<boolean>) => {
|
||||
if (directionCache) {
|
||||
each(lifecycles, (lifecycle) => {
|
||||
lifecycle._onDirectionChanged && lifecycle._onDirectionChanged(directionCache);
|
||||
@@ -65,13 +32,13 @@ const OverlayScrollbars = (target: OSTarget, options?: any, extensions?: any): v
|
||||
});
|
||||
};
|
||||
|
||||
createSizeObserver(host, onSizeChanged, { _appear: true, _direction: true });
|
||||
createTrinsicObserver(host, onTrinsicChanged);
|
||||
createDOMObserver(host, () => {
|
||||
createSizeObserver(_host, onSizeChanged, { _appear: true, _direction: true });
|
||||
createTrinsicObserver(_host, onTrinsicChanged);
|
||||
createDOMObserver(_host, () => {
|
||||
return null;
|
||||
});
|
||||
createDOMObserver(
|
||||
content,
|
||||
_content || _viewport,
|
||||
() => {
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
isHTMLElement,
|
||||
appendChildren,
|
||||
is,
|
||||
createDiv,
|
||||
contents,
|
||||
insertAfter,
|
||||
addClass,
|
||||
parent,
|
||||
isUndefined,
|
||||
removeElements,
|
||||
removeClass,
|
||||
push,
|
||||
runEach,
|
||||
} from 'support';
|
||||
import { classNameHost, classNamePadding, classNameViewport, classNameContent } from 'classnames';
|
||||
import { OSTarget, OSTargetObject, InternalVersionOf, OSTargetElement } from 'typings';
|
||||
|
||||
export interface OSTargetContext {
|
||||
_isTextarea: boolean;
|
||||
_isBody: boolean;
|
||||
_htmlElm: HTMLHtmlElement;
|
||||
_bodyElm: HTMLBodyElement;
|
||||
_windowElm: Window;
|
||||
_documentElm: HTMLDocument;
|
||||
}
|
||||
|
||||
export interface PreparedOSTargetObject extends Required<InternalVersionOf<OSTargetObject>> {
|
||||
_host: HTMLElement;
|
||||
}
|
||||
|
||||
export interface StructureSetup {
|
||||
_targetObj: PreparedOSTargetObject;
|
||||
_targetCtx: OSTargetContext;
|
||||
_destroy: () => void;
|
||||
}
|
||||
|
||||
const unwrap = (elm: HTMLElement | null | undefined) => {
|
||||
appendChildren(parent(elm), contents(elm));
|
||||
removeElements(elm);
|
||||
};
|
||||
|
||||
export const createStructureSetup = (target: OSTarget | OSTargetObject): StructureSetup => {
|
||||
const targetIsElm = isHTMLElement(target);
|
||||
const osTargetObj: InternalVersionOf<OSTargetObject> = targetIsElm
|
||||
? ({} as InternalVersionOf<OSTargetObject>)
|
||||
: {
|
||||
_host: (target as OSTargetObject).host,
|
||||
_target: (target as OSTargetObject).target,
|
||||
_padding: (target as OSTargetObject).padding,
|
||||
_viewport: (target as OSTargetObject).viewport,
|
||||
_content: (target as OSTargetObject).content,
|
||||
};
|
||||
|
||||
if (targetIsElm) {
|
||||
const padding = createDiv(classNamePadding);
|
||||
const viewport = createDiv(classNameViewport);
|
||||
const content = createDiv(classNameContent);
|
||||
|
||||
appendChildren(padding, viewport);
|
||||
appendChildren(viewport, content);
|
||||
|
||||
osTargetObj._target = target as OSTargetElement;
|
||||
osTargetObj._padding = padding;
|
||||
osTargetObj._viewport = viewport;
|
||||
osTargetObj._content = content;
|
||||
}
|
||||
|
||||
let { _target, _padding, _viewport, _content } = osTargetObj;
|
||||
let destroyFns: (() => any)[] = [];
|
||||
const isTextarea = is(_target, 'textarea');
|
||||
const isBody = !isTextarea && is(_target, 'body');
|
||||
const _host = (isTextarea ? osTargetObj._host || createDiv() : _target) as HTMLElement;
|
||||
const getTargetContents = (contentSlot: HTMLElement) => (isTextarea ? (_target as HTMLTextAreaElement) : contents(contentSlot as HTMLElement));
|
||||
|
||||
const ownerDocument: HTMLDocument = _target.ownerDocument;
|
||||
const bodyElm = ownerDocument.body as HTMLBodyElement;
|
||||
const wnd = ownerDocument.defaultView as Window;
|
||||
const isTextareaHostGenerated = isTextarea && _host !== osTargetObj._host;
|
||||
|
||||
// only insert host for textarea after target if it was generated
|
||||
if (isTextareaHostGenerated) {
|
||||
insertAfter(_target, _host);
|
||||
|
||||
push(destroyFns, () => {
|
||||
insertAfter(_host, _target);
|
||||
removeElements(_host);
|
||||
});
|
||||
}
|
||||
|
||||
if (targetIsElm) {
|
||||
appendChildren(_content!, getTargetContents(_target));
|
||||
appendChildren(_host, _padding);
|
||||
|
||||
push(destroyFns, () => {
|
||||
appendChildren(_host, contents(_content));
|
||||
removeElements(_padding);
|
||||
removeClass(_host, classNameHost);
|
||||
});
|
||||
} else {
|
||||
const contentContainingElm = _content || _viewport || _padding || _host;
|
||||
const createPadding = isUndefined(_padding);
|
||||
const createViewport = isUndefined(_viewport);
|
||||
const createContent = isUndefined(_content);
|
||||
const targetContents = getTargetContents(contentContainingElm);
|
||||
|
||||
_padding = osTargetObj._padding = createPadding ? createDiv() : _padding;
|
||||
_viewport = osTargetObj._viewport = createViewport ? createDiv() : _viewport;
|
||||
_content = osTargetObj._content = createContent ? createDiv() : _content;
|
||||
|
||||
appendChildren(_host, _padding);
|
||||
appendChildren(_padding || _host, _viewport);
|
||||
appendChildren(_viewport, _content);
|
||||
|
||||
const contentSlot = _content || _viewport;
|
||||
appendChildren(contentSlot, targetContents);
|
||||
|
||||
push(destroyFns, () => {
|
||||
if (createContent) {
|
||||
unwrap(_content);
|
||||
}
|
||||
if (createViewport) {
|
||||
unwrap(_viewport);
|
||||
}
|
||||
if (createPadding) {
|
||||
unwrap(_padding);
|
||||
}
|
||||
removeClass(_host, classNameHost);
|
||||
removeClass(_padding, classNamePadding);
|
||||
removeClass(_viewport, classNameViewport);
|
||||
removeClass(_content, classNameContent);
|
||||
});
|
||||
}
|
||||
|
||||
addClass(_host, classNameHost);
|
||||
addClass(_padding, classNamePadding);
|
||||
addClass(_viewport, classNameViewport);
|
||||
addClass(_content, classNameContent);
|
||||
|
||||
const ctx: OSTargetContext = {
|
||||
_windowElm: wnd,
|
||||
_documentElm: ownerDocument,
|
||||
_htmlElm: parent(bodyElm) as HTMLHtmlElement,
|
||||
_bodyElm: bodyElm,
|
||||
_isTextarea: isTextarea,
|
||||
_isBody: isBody,
|
||||
};
|
||||
// @ts-ignore
|
||||
const obj: PreparedOSTargetObject = {
|
||||
...osTargetObj,
|
||||
_host,
|
||||
};
|
||||
|
||||
return {
|
||||
_targetObj: obj,
|
||||
_targetCtx: ctx,
|
||||
_destroy: () => {
|
||||
runEach(destroyFns);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -3,7 +3,11 @@ import { each } from 'support/utils/array';
|
||||
import { keys } from 'support/utils/object';
|
||||
|
||||
const rnothtmlwhite = /[^\x20\t\r\n\f]+/g;
|
||||
const classListAction = (elm: Element | null, className: string, action: (elmClassList: DOMTokenList, clazz: string) => boolean | void): boolean => {
|
||||
const classListAction = (
|
||||
elm: Element | null | undefined,
|
||||
className: string,
|
||||
action: (elmClassList: DOMTokenList, clazz: string) => boolean | void
|
||||
): boolean => {
|
||||
let clazz: string;
|
||||
let i = 0;
|
||||
let result = false;
|
||||
@@ -23,7 +27,7 @@ const classListAction = (elm: Element | null, className: string, action: (elmCla
|
||||
* @param elm The element.
|
||||
* @param className The class name(s).
|
||||
*/
|
||||
export const hasClass = (elm: Element | null, className: string): boolean =>
|
||||
export const hasClass = (elm: Element | null | undefined, className: string): boolean =>
|
||||
classListAction(elm, className, (classList, clazz) => classList.contains(clazz));
|
||||
|
||||
/**
|
||||
@@ -31,7 +35,7 @@ export const hasClass = (elm: Element | null, className: string): boolean =>
|
||||
* @param elm The element.
|
||||
* @param className The class name(s) which shall be added. (separated by spaces)
|
||||
*/
|
||||
export const addClass = (elm: Element | null, className: string): void => {
|
||||
export const addClass = (elm: Element | null | undefined, className: string): void => {
|
||||
classListAction(elm, className, (classList, clazz) => classList.add(clazz));
|
||||
};
|
||||
|
||||
@@ -40,7 +44,7 @@ export const addClass = (elm: Element | null, className: string): void => {
|
||||
* @param elm The element.
|
||||
* @param className The class name(s) which shall be removed. (separated by spaces)
|
||||
*/
|
||||
export const removeClass = (elm: Element | null, className: string): void => {
|
||||
export const removeClass = (elm: Element | null | undefined, className: string): void => {
|
||||
classListAction(elm, className, (classList, clazz) => classList.remove(clazz));
|
||||
};
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export const windowSize = (): WH => ({
|
||||
* Returns the scroll- width and height of the passed element. If the element is null the width and height values are 0.
|
||||
* @param elm The element of which the scroll- width and height shall be returned.
|
||||
*/
|
||||
export const offsetSize = (elm: HTMLElement | null): WH =>
|
||||
export const offsetSize = (elm: HTMLElement | null | undefined): WH =>
|
||||
elm
|
||||
? {
|
||||
w: elm.offsetWidth,
|
||||
@@ -33,7 +33,7 @@ export const offsetSize = (elm: HTMLElement | null): WH =>
|
||||
* Returns the client- width and height of the passed element. If the element is null the width and height values are 0.
|
||||
* @param elm The element of which the client- width and height shall be returned.
|
||||
*/
|
||||
export const clientSize = (elm: HTMLElement | null): WH =>
|
||||
export const clientSize = (elm: HTMLElement | null | undefined): WH =>
|
||||
elm
|
||||
? {
|
||||
w: elm.clientWidth,
|
||||
@@ -45,7 +45,7 @@ export const clientSize = (elm: HTMLElement | null): WH =>
|
||||
* Returns the client- width and height of the passed element. If the element is null the width and height values are 0.
|
||||
* @param elm The element of which the client- width and height shall be returned.
|
||||
*/
|
||||
export const scrollSize = (elm: HTMLElement | null): WH =>
|
||||
export const scrollSize = (elm: HTMLElement | null | undefined): WH =>
|
||||
elm
|
||||
? {
|
||||
w: elm.scrollWidth,
|
||||
@@ -63,4 +63,4 @@ export const getBoundingClientRect = (elm: HTMLElement): DOMRect => elm.getBound
|
||||
* Determines whether the passed element has any dimensions.
|
||||
* @param elm The element.
|
||||
*/
|
||||
export const hasDimensions = (elm: HTMLElement | null): boolean => (elm ? elementHasDimensions(elm as HTMLElement) : false);
|
||||
export const hasDimensions = (elm: HTMLElement | null | undefined): boolean => (elm ? elementHasDimensions(elm as HTMLElement) : false);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { isArrayLike } from 'support/utils/types';
|
||||
import { each, from } from 'support/utils/array';
|
||||
import { parent } from 'support/dom/traversal';
|
||||
|
||||
type NodeCollection = ArrayLike<Node> | Node | undefined | null;
|
||||
type NodeCollection = ArrayLike<Node> | Node | null | undefined;
|
||||
|
||||
/**
|
||||
* Inserts Nodes before the given preferredAnchor element.
|
||||
@@ -10,10 +10,10 @@ type NodeCollection = ArrayLike<Node> | Node | undefined | null;
|
||||
* @param preferredAnchor The element before which the Nodes shall be inserted or null if the elements shall be appended at the end.
|
||||
* @param insertedElms The Nodes which shall be inserted.
|
||||
*/
|
||||
const before = (parentElm: Node | null, preferredAnchor: Node | null, insertedElms: NodeCollection): void => {
|
||||
const before = (parentElm: Node | null | undefined, preferredAnchor: Node | null | undefined, insertedElms: NodeCollection): void => {
|
||||
if (insertedElms) {
|
||||
let anchor: Node | null = preferredAnchor;
|
||||
let fragment: DocumentFragment | Node | undefined | null;
|
||||
let anchor: Node | null | undefined = preferredAnchor;
|
||||
let fragment: DocumentFragment | Node | null | undefined;
|
||||
|
||||
// parent must be defined
|
||||
if (parentElm) {
|
||||
@@ -40,7 +40,7 @@ const before = (parentElm: Node | null, preferredAnchor: Node | null, insertedEl
|
||||
}
|
||||
}
|
||||
|
||||
parentElm.insertBefore(fragment, anchor);
|
||||
parentElm.insertBefore(fragment, anchor || null);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -50,7 +50,7 @@ const before = (parentElm: Node | null, preferredAnchor: Node | null, insertedEl
|
||||
* @param node The Node to which the children shall be appended.
|
||||
* @param children The Nodes which shall be appended.
|
||||
*/
|
||||
export const appendChildren = (node: Node | null, children: NodeCollection): void => {
|
||||
export const appendChildren = (node: Node | null | undefined, children: NodeCollection): void => {
|
||||
before(node, null, children);
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ export const appendChildren = (node: Node | null, children: NodeCollection): voi
|
||||
* @param node The Node to which the children shall be prepended.
|
||||
* @param children The Nodes which shall be prepended.
|
||||
*/
|
||||
export const prependChildren = (node: Node | null, children: NodeCollection): void => {
|
||||
export const prependChildren = (node: Node | null | undefined, children: NodeCollection): void => {
|
||||
before(node, node && node.firstChild, children);
|
||||
};
|
||||
|
||||
@@ -68,7 +68,7 @@ export const prependChildren = (node: Node | null, children: NodeCollection): vo
|
||||
* @param node The Node before which the given Nodes shall be inserted.
|
||||
* @param insertedNodes The Nodes which shall be inserted.
|
||||
*/
|
||||
export const insertBefore = (node: Node | null, insertedNodes: NodeCollection): void => {
|
||||
export const insertBefore = (node: Node | null | undefined, insertedNodes: NodeCollection): void => {
|
||||
before(parent(node), node, insertedNodes);
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ export const insertBefore = (node: Node | null, insertedNodes: NodeCollection):
|
||||
* @param node The Node after which the given Nodes shall be inserted.
|
||||
* @param insertedNodes The Nodes which shall be inserted.
|
||||
*/
|
||||
export const insertAfter = (node: Node | null, insertedNodes: NodeCollection): void => {
|
||||
export const insertAfter = (node: Node | null | undefined, insertedNodes: NodeCollection): void => {
|
||||
before(parent(node), node && node.nextSibling, insertedNodes);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const zeroObj: XY = {
|
||||
* Returns the offset- left and top coordinates of the passed element relative to the document. If the element is null the top and left values are 0.
|
||||
* @param elm The element of which the offset- top and left coordinates shall be returned.
|
||||
*/
|
||||
export const absoluteCoordinates = (elm: HTMLElement | null): XY => {
|
||||
export const absoluteCoordinates = (elm: HTMLElement | null | undefined): XY => {
|
||||
const rect = elm ? getBoundingClientRect(elm) : 0;
|
||||
return rect
|
||||
? {
|
||||
@@ -28,7 +28,7 @@ export const absoluteCoordinates = (elm: HTMLElement | null): XY => {
|
||||
* Returns the offset- left and top coordinates of the passed element. If the element is null the top and left values are 0.
|
||||
* @param elm The element of which the offset- top and left coordinates shall be returned.
|
||||
*/
|
||||
export const offsetCoordinates = (elm: HTMLElement | null): XY =>
|
||||
export const offsetCoordinates = (elm: HTMLElement | null | undefined): XY =>
|
||||
elm
|
||||
? {
|
||||
x: elm.offsetLeft,
|
||||
|
||||
@@ -36,7 +36,7 @@ const adaptCSSVal = (prop: string, val: string | number): string | number => (!c
|
||||
const getCSSVal = (elm: HTMLElement, computedStyle: CSSStyleDeclaration, prop: string): string =>
|
||||
/* istanbul ignore next */
|
||||
computedStyle != null ? computedStyle.getPropertyValue(prop) : elm.style[prop];
|
||||
const setCSSVal = (elm: HTMLElement | null, prop: string, val: string | number): void => {
|
||||
const setCSSVal = (elm: HTMLElement | null | undefined, prop: string, val: string | number): void => {
|
||||
try {
|
||||
if (elm && elm.style[prop] !== undefined) {
|
||||
elm.style[prop] = adaptCSSVal(prop, val);
|
||||
@@ -49,10 +49,10 @@ const setCSSVal = (elm: HTMLElement | null, prop: string, val: string | number):
|
||||
* @param elm The element to which the styles shall be applied to / be read from.
|
||||
* @param styles The styles which shall be set or read.
|
||||
*/
|
||||
export function style(elm: HTMLElement | null, styles: CssStyles): void;
|
||||
export function style(elm: HTMLElement | null, styles: string): string;
|
||||
export function style(elm: HTMLElement | null, styles: Array<string> | string): { [key: string]: string };
|
||||
export function style(elm: HTMLElement | null, styles: CssStyles | Array<string> | string): { [key: string]: string } | string | void {
|
||||
export function style(elm: HTMLElement | null | undefined, styles: CssStyles): void;
|
||||
export function style(elm: HTMLElement | null | undefined, styles: string): string;
|
||||
export function style(elm: HTMLElement | null | undefined, styles: Array<string> | string): { [key: string]: string };
|
||||
export function style(elm: HTMLElement | null | undefined, styles: CssStyles | Array<string> | string): { [key: string]: string } | string | void {
|
||||
const getSingleStyle = isString(styles);
|
||||
const getStyles = isArray(styles) || getSingleStyle;
|
||||
|
||||
@@ -84,7 +84,7 @@ export const hide = (elm: HTMLElement | null): void => {
|
||||
* Shows the passed element (display: block).
|
||||
* @param elm The element which shall be shown.
|
||||
*/
|
||||
export const show = (elm: HTMLElement | null): void => {
|
||||
export const show = (elm: HTMLElement | null | undefined): void => {
|
||||
style(elm, { display: 'block' });
|
||||
};
|
||||
|
||||
@@ -93,7 +93,7 @@ export const show = (elm: HTMLElement | null): void => {
|
||||
* @param elm
|
||||
* @param property
|
||||
*/
|
||||
export const topRightBottomLeft = (elm: HTMLElement | null, property?: string): TRBL => {
|
||||
export const topRightBottomLeft = (elm: HTMLElement | null | undefined, property?: string): TRBL => {
|
||||
const finalProp = property || '';
|
||||
const top = `${finalProp}-top`;
|
||||
const right = `${finalProp}-right`;
|
||||
|
||||
@@ -4,15 +4,17 @@ export type OSTargetElement = HTMLElement | HTMLTextAreaElement;
|
||||
|
||||
export interface OSTargetObject {
|
||||
target: OSTargetElement;
|
||||
host: HTMLElement;
|
||||
padding: HTMLElement;
|
||||
viewport: HTMLElement;
|
||||
content: HTMLElement;
|
||||
host?: HTMLElement;
|
||||
padding?: HTMLElement | null;
|
||||
viewport?: HTMLElement;
|
||||
content?: HTMLElement | null;
|
||||
}
|
||||
|
||||
export type OSTarget = OSTargetElement | OSTargetObject;
|
||||
export type InternalVersionOf<T> = {
|
||||
[K in keyof T as `_${Uncapitalize<string & K>}`]: T[K];
|
||||
};
|
||||
|
||||
export type CSSDirection = 'ltr' | 'rtl';
|
||||
export type OSTarget = OSTargetElement | OSTargetObject;
|
||||
|
||||
/*
|
||||
export namespace OverlayScrollbars {
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
import { createStructureSetup, StructureSetup } from 'setups/structureSetup';
|
||||
|
||||
const textareaId = 'textarea';
|
||||
const textareaHostId = 'host';
|
||||
const elementId = 'target';
|
||||
const dynamicContent = 'text<p>paragraph</p>';
|
||||
const textareaContent = `<textarea id="${textareaId}">text</textarea>`;
|
||||
const getSnapshot = () => document.body.innerHTML;
|
||||
const getTarget = (textarea?: boolean) => document.getElementById(textarea ? textareaId : elementId)!;
|
||||
const fillBody = (textarea?: boolean, customDOM?: (content: string, hostId: string) => string) => {
|
||||
document.body.innerHTML = `
|
||||
<nav></nav>
|
||||
${
|
||||
customDOM
|
||||
? customDOM(textarea ? textareaContent : dynamicContent, textarea ? textareaHostId : elementId)
|
||||
: textarea
|
||||
? textareaContent
|
||||
: `<div id="${elementId}">${dynamicContent}</div>`
|
||||
}
|
||||
<footer></footer>
|
||||
`;
|
||||
return getSnapshot();
|
||||
};
|
||||
const clearBody = () => {
|
||||
document.body.innerHTML = '';
|
||||
};
|
||||
|
||||
const getElements = (textarea?: boolean) => {
|
||||
const target = getTarget(textarea);
|
||||
const host = document.querySelector('.os-host')!;
|
||||
const padding = document.querySelector('.os-padding')!;
|
||||
const viewport = document.querySelector('.os-viewport')!;
|
||||
const content = document.querySelector('.os-content')!;
|
||||
|
||||
return {
|
||||
target,
|
||||
host,
|
||||
padding,
|
||||
viewport,
|
||||
content,
|
||||
};
|
||||
};
|
||||
|
||||
const assertCorrectDOMStructure = (textarea?: boolean) => {
|
||||
const { target, host, padding, viewport, content } = getElements(textarea);
|
||||
|
||||
expect(host).toBeTruthy();
|
||||
expect(viewport).toBeTruthy();
|
||||
expect(viewport.parentElement).toBe(padding || host);
|
||||
|
||||
if (content) {
|
||||
expect(content.parentElement).toBe(viewport);
|
||||
}
|
||||
if (padding) {
|
||||
expect(padding.parentElement).toBe(host);
|
||||
}
|
||||
|
||||
expect(host.parentElement).toBe(document.body);
|
||||
expect(host.previousElementSibling).toBe(document.querySelector('nav'));
|
||||
expect(host.nextElementSibling).toBe(document.querySelector('footer'));
|
||||
|
||||
const contentElm = content || viewport;
|
||||
if (textarea) {
|
||||
expect(target.parentElement).toBe(contentElm);
|
||||
expect(contentElm.innerHTML).toBe(textareaContent);
|
||||
} else {
|
||||
expect(target).toBe(host);
|
||||
expect(contentElm.innerHTML).toBe(dynamicContent);
|
||||
}
|
||||
};
|
||||
|
||||
const assertCorrectSetup = (textarea: boolean, setup: StructureSetup) => {
|
||||
const { _targetObj, _targetCtx, _destroy } = setup;
|
||||
const { _target, _host, _padding, _viewport, _content } = _targetObj;
|
||||
const { target, host, padding, viewport, content } = getElements(textarea);
|
||||
const isTextarea = target.matches('textarea');
|
||||
const isBody = target.matches('body');
|
||||
|
||||
expect(textarea).toBe(isTextarea);
|
||||
|
||||
expect(_target).toBe(target);
|
||||
expect(_host).toBe(host);
|
||||
|
||||
if (padding || _padding) {
|
||||
expect(_padding).toBe(padding);
|
||||
} else {
|
||||
expect(padding).toBeFalsy();
|
||||
expect(_padding).toBeFalsy();
|
||||
}
|
||||
|
||||
if (viewport || _viewport) {
|
||||
expect(_viewport).toBe(viewport);
|
||||
} else {
|
||||
expect(viewport).toBeFalsy();
|
||||
expect(_viewport).toBeFalsy();
|
||||
}
|
||||
|
||||
if (content || _content) {
|
||||
expect(_content).toBe(content);
|
||||
} else {
|
||||
expect(content).toBeFalsy();
|
||||
expect(_content).toBeFalsy();
|
||||
}
|
||||
|
||||
const { _isTextarea, _isBody, _bodyElm, _htmlElm, _documentElm, _windowElm } = _targetCtx;
|
||||
|
||||
expect(_isTextarea).toBe(isTextarea);
|
||||
expect(_isBody).toBe(isBody);
|
||||
expect(_windowElm).toBe(document.defaultView);
|
||||
expect(_documentElm).toBe(document);
|
||||
expect(_htmlElm).toBe(document.body.parentElement);
|
||||
expect(_bodyElm).toBe(document.body);
|
||||
|
||||
expect(typeof _destroy).toBe('function');
|
||||
|
||||
return setup;
|
||||
};
|
||||
|
||||
const assertCorrectDestroy = (snapshot: string, setup: StructureSetup) => {
|
||||
const { _destroy } = setup;
|
||||
|
||||
_destroy();
|
||||
|
||||
// remove empty class attr
|
||||
const elms = document.querySelectorAll('*');
|
||||
Array.from(elms).forEach((elm) => {
|
||||
const classAttr = elm.getAttribute('class');
|
||||
if (classAttr === '') {
|
||||
elm.removeAttribute('class');
|
||||
}
|
||||
});
|
||||
|
||||
expect(snapshot).toBe(getSnapshot());
|
||||
};
|
||||
|
||||
describe('structureSetup', () => {
|
||||
afterEach(() => clearBody());
|
||||
|
||||
[false, true].forEach((isTextarea) => {
|
||||
describe(isTextarea ? 'textarea' : 'element', () => {
|
||||
describe('basic', () => {
|
||||
test('Element', () => {
|
||||
const snapshot = fillBody(isTextarea);
|
||||
const setup = assertCorrectSetup(isTextarea, createStructureSetup(getTarget(isTextarea)));
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('Object', () => {
|
||||
const snapshot = fillBody(isTextarea);
|
||||
const setup = assertCorrectSetup(isTextarea, createStructureSetup({ target: getTarget(isTextarea) }));
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
});
|
||||
|
||||
describe('complex', () => {
|
||||
describe('single assigned', () => {
|
||||
test('padding', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="padding">${content}</div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
padding: document.querySelector<HTMLElement>('#padding')!,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('viewport', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
viewport: document.querySelector<HTMLElement>('#viewport')!,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('content', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="content">${content}</div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
content: document.querySelector<HTMLElement>('#content')!,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple assigned', () => {
|
||||
test('padding viewport content', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="padding"><div id="viewport"><div id="content">${content}</div></div></div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
padding: document.querySelector<HTMLElement>('#padding')!,
|
||||
viewport: document.querySelector<HTMLElement>('#viewport')!,
|
||||
content: document.querySelector<HTMLElement>('#content')!,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('padding viewport', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
padding: document.querySelector<HTMLElement>('#padding')!,
|
||||
viewport: document.querySelector<HTMLElement>('#viewport')!,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('padding content', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="padding"><div id="content">${content}</div></div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
padding: document.querySelector<HTMLElement>('#padding')!,
|
||||
content: document.querySelector<HTMLElement>('#content')!,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('viewport content', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
viewport: document.querySelector<HTMLElement>('#viewport')!,
|
||||
content: document.querySelector<HTMLElement>('#content')!,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
});
|
||||
|
||||
describe('single null', () => {
|
||||
test('padding', () => {
|
||||
const snapshot = fillBody(isTextarea);
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
target: getTarget(isTextarea),
|
||||
padding: null,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('content', () => {
|
||||
const snapshot = fillBody(isTextarea);
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
target: getTarget(isTextarea),
|
||||
content: null,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple null', () => {
|
||||
test('padding & content', () => {
|
||||
const snapshot = fillBody(isTextarea);
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
target: getTarget(isTextarea),
|
||||
padding: null,
|
||||
content: null,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed', () => {
|
||||
test('null: padding & content | assigned: viewport', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
padding: null,
|
||||
viewport: document.querySelector<HTMLElement>('#viewport')!,
|
||||
content: null,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('null: padding | assigned: content', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="content">${content}</div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
padding: null,
|
||||
content: document.querySelector<HTMLElement>('#content')!,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('null: padding | assigned: viewport', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
padding: null,
|
||||
viewport: document.querySelector<HTMLElement>('#viewport')!,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('null: padding | assigned: viewport & content', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
viewport: document.querySelector<HTMLElement>('#viewport')!,
|
||||
padding: null,
|
||||
content: document.querySelector<HTMLElement>('#content')!,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('null: content | assigned: padding', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="padding">${content}</div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
padding: document.querySelector<HTMLElement>('#padding')!,
|
||||
content: null,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('null: content | assigned: viewport', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
viewport: document.querySelector<HTMLElement>('#viewport')!,
|
||||
content: null,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
|
||||
test('null: content | assigned: padding & viewport', () => {
|
||||
const snapshot = fillBody(isTextarea, (content, hostId) => {
|
||||
return `<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`;
|
||||
});
|
||||
const setup = assertCorrectSetup(
|
||||
isTextarea,
|
||||
createStructureSetup({
|
||||
host: document.querySelector<HTMLElement>('#host')!,
|
||||
target: getTarget(isTextarea),
|
||||
padding: document.querySelector<HTMLElement>('#padding')!,
|
||||
viewport: document.querySelector<HTMLElement>('#viewport')!,
|
||||
content: null,
|
||||
})
|
||||
);
|
||||
assertCorrectDOMStructure(isTextarea);
|
||||
assertCorrectDestroy(snapshot, setup);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user