From f5efd56f7014b8a905a3d3eb46f030ec8b2ff20b Mon Sep 17 00:00:00 2001 From: Rene Date: Sun, 14 Feb 2021 21:30:46 +0100 Subject: [PATCH] add structureSetup --- .../src/lifecycles/lifecycleBase.ts | 4 +- .../src/lifecycles/structureLifecycle.ts | 18 +- .../overlayscrollbars/OverlayScrollbars.ts | 57 +-- .../src/setups/structureSetup.ts | 161 +++++++ .../src/support/dom/class.ts | 12 +- .../src/support/dom/dimensions.ts | 8 +- .../src/support/dom/manipulation.ts | 18 +- .../src/support/dom/offset.ts | 4 +- .../src/support/dom/style.ts | 14 +- packages/overlayscrollbars/src/typings.ts | 14 +- .../tests/jsdom/setups/structureSetup.test.ts | 449 ++++++++++++++++++ 11 files changed, 671 insertions(+), 88 deletions(-) create mode 100644 packages/overlayscrollbars/src/setups/structureSetup.ts create mode 100644 packages/overlayscrollbars/tests/jsdom/setups/structureSetup.test.ts diff --git a/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts b/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts index 948587f..d415707 100644 --- a/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts +++ b/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts @@ -8,7 +8,7 @@ import { hasOwnProperty, isEmptyObject, } from 'support'; -import { CSSDirection, PlainObject } from 'typings'; +import { PlainObject } from 'typings'; interface LifecycleBaseUpdateHints { _force?: boolean; @@ -23,7 +23,7 @@ export interface LifecycleBase { export interface Lifecycle extends LifecycleBase { _destruct(): void; _onSizeChanged?(): void; - _onDirectionChanged?(directionCache: Cache): void; + _onDirectionChanged?(directionCache: Cache): void; _onTrinsicChanged?(widthIntrinsic: boolean, heightIntrinsicCache: Cache): void; } diff --git a/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts b/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts index d05608f..27e40f9 100644 --- a/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts +++ b/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts @@ -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 => { - 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, { _contentScrollSize: WH; _viewportSize: WH }>( (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) => { const { _changed, _value } = heightIntrinsicCache; if (_changed) { - style(content, { height: _value ? 'auto' : '100%' }); + style(_content, { height: _value ? 'auto' : '100%' }); } }; diff --git a/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts b/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts index 22bef22..dcc538c 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts @@ -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[] = []; - 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) => { + const onSizeChanged = (directionCache?: Cache) => { 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; }, diff --git a/packages/overlayscrollbars/src/setups/structureSetup.ts b/packages/overlayscrollbars/src/setups/structureSetup.ts new file mode 100644 index 0000000..0984934 --- /dev/null +++ b/packages/overlayscrollbars/src/setups/structureSetup.ts @@ -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> { + _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 = targetIsElm + ? ({} as InternalVersionOf) + : { + _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); + }, + }; +}; diff --git a/packages/overlayscrollbars/src/support/dom/class.ts b/packages/overlayscrollbars/src/support/dom/class.ts index f1a3ce8..7708a34 100644 --- a/packages/overlayscrollbars/src/support/dom/class.ts +++ b/packages/overlayscrollbars/src/support/dom/class.ts @@ -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)); }; diff --git a/packages/overlayscrollbars/src/support/dom/dimensions.ts b/packages/overlayscrollbars/src/support/dom/dimensions.ts index 9ae61f0..f0599bf 100644 --- a/packages/overlayscrollbars/src/support/dom/dimensions.ts +++ b/packages/overlayscrollbars/src/support/dom/dimensions.ts @@ -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); diff --git a/packages/overlayscrollbars/src/support/dom/manipulation.ts b/packages/overlayscrollbars/src/support/dom/manipulation.ts index fc8831f..14b4ba6 100644 --- a/packages/overlayscrollbars/src/support/dom/manipulation.ts +++ b/packages/overlayscrollbars/src/support/dom/manipulation.ts @@ -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 | undefined | null; +type NodeCollection = ArrayLike | Node | null | undefined; /** * Inserts Nodes before the given preferredAnchor element. @@ -10,10 +10,10 @@ type NodeCollection = ArrayLike | 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); }; diff --git a/packages/overlayscrollbars/src/support/dom/offset.ts b/packages/overlayscrollbars/src/support/dom/offset.ts index 783cfa6..9a5b2ac 100644 --- a/packages/overlayscrollbars/src/support/dom/offset.ts +++ b/packages/overlayscrollbars/src/support/dom/offset.ts @@ -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, diff --git a/packages/overlayscrollbars/src/support/dom/style.ts b/packages/overlayscrollbars/src/support/dom/style.ts index f3c0f73..b15b305 100644 --- a/packages/overlayscrollbars/src/support/dom/style.ts +++ b/packages/overlayscrollbars/src/support/dom/style.ts @@ -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): { [key: string]: string }; -export function style(elm: HTMLElement | null, styles: CssStyles | Array | 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): { [key: string]: string }; +export function style(elm: HTMLElement | null | undefined, styles: CssStyles | Array | 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`; diff --git a/packages/overlayscrollbars/src/typings.ts b/packages/overlayscrollbars/src/typings.ts index 081d53e..fe31254 100644 --- a/packages/overlayscrollbars/src/typings.ts +++ b/packages/overlayscrollbars/src/typings.ts @@ -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 = { + [K in keyof T as `_${Uncapitalize}`]: T[K]; +}; -export type CSSDirection = 'ltr' | 'rtl'; +export type OSTarget = OSTargetElement | OSTargetObject; /* export namespace OverlayScrollbars { diff --git a/packages/overlayscrollbars/tests/jsdom/setups/structureSetup.test.ts b/packages/overlayscrollbars/tests/jsdom/setups/structureSetup.test.ts new file mode 100644 index 0000000..e067831 --- /dev/null +++ b/packages/overlayscrollbars/tests/jsdom/setups/structureSetup.test.ts @@ -0,0 +1,449 @@ +import { createStructureSetup, StructureSetup } from 'setups/structureSetup'; + +const textareaId = 'textarea'; +const textareaHostId = 'host'; +const elementId = 'target'; +const dynamicContent = 'text

paragraph

'; +const textareaContent = ``; +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 = ` + + ${ + customDOM + ? customDOM(textarea ? textareaContent : dynamicContent, textarea ? textareaHostId : elementId) + : textarea + ? textareaContent + : `
${dynamicContent}
` + } +
+ `; + 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 `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + padding: document.querySelector('#padding')!, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + + test('viewport', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + viewport: document.querySelector('#viewport')!, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + + test('content', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + content: document.querySelector('#content')!, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + }); + + describe('multiple assigned', () => { + test('padding viewport content', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + padding: document.querySelector('#padding')!, + viewport: document.querySelector('#viewport')!, + content: document.querySelector('#content')!, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + + test('padding viewport', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + padding: document.querySelector('#padding')!, + viewport: document.querySelector('#viewport')!, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + + test('padding content', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + padding: document.querySelector('#padding')!, + content: document.querySelector('#content')!, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + + test('viewport content', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + viewport: document.querySelector('#viewport')!, + content: document.querySelector('#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 `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + padding: null, + viewport: document.querySelector('#viewport')!, + content: null, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + + test('null: padding | assigned: content', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + padding: null, + content: document.querySelector('#content')!, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + + test('null: padding | assigned: viewport', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + padding: null, + viewport: document.querySelector('#viewport')!, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + + test('null: padding | assigned: viewport & content', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + viewport: document.querySelector('#viewport')!, + padding: null, + content: document.querySelector('#content')!, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + + test('null: content | assigned: padding', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + padding: document.querySelector('#padding')!, + content: null, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + + test('null: content | assigned: viewport', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + viewport: document.querySelector('#viewport')!, + content: null, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + + test('null: content | assigned: padding & viewport', () => { + const snapshot = fillBody(isTextarea, (content, hostId) => { + return `
${content}
`; + }); + const setup = assertCorrectSetup( + isTextarea, + createStructureSetup({ + host: document.querySelector('#host')!, + target: getTarget(isTextarea), + padding: document.querySelector('#padding')!, + viewport: document.querySelector('#viewport')!, + content: null, + }) + ); + assertCorrectDOMStructure(isTextarea); + assertCorrectDestroy(snapshot, setup); + }); + }); + }); + }); + }); +});