add structureSetup

This commit is contained in:
Rene
2021-02-14 21:30:46 +01:00
parent a6c7c90147
commit f5efd56f70
11 changed files with 671 additions and 88 deletions
@@ -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`;
+8 -6
View File
@@ -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);
});
});
});
});
});
});