This commit is contained in:
Rene
2020-12-16 11:21:59 +01:00
parent fa956c4b1d
commit a8fa1cb6db
13 changed files with 169 additions and 95 deletions
@@ -26,10 +26,13 @@ interface AbstractLifecycle<O extends PlainObject> {
export interface Lifecycle<T extends PlainObject> extends AbstractLifecycle<T> {
_destruct(): void;
_onSizeChanged?(): void;
_onDirectionChanged?(direction: 'ltr' | 'rtl'): void;
_onTrinsicChanged?(widthIntrinsic: boolean, heightIntrinsic: boolean): void;
}
export interface LifecycleBase<O extends PlainObject, C extends PlainObject> extends AbstractLifecycle<O> {
_cacheChange(cachePropsToUpdate?: CachePropsToUpdate<C>): void;
_updateCache(cachePropsToUpdate?: CachePropsToUpdate<C>): void;
}
/**
@@ -80,7 +83,7 @@ export const createLifecycleBase = <O, C>(
_update: (force?: boolean) => {
update({ _force: !!force });
},
_cacheChange: (cachePropsToUpdate?: CachePropsToUpdate<C>) => {
_updateCache: (cachePropsToUpdate?: CachePropsToUpdate<C>) => {
update({ _changedCache: cachePropsToUpdate });
},
};
@@ -1,21 +1,17 @@
import {
cssProperty,
createDOM,
runEach,
contents,
appendChildren,
removeElements,
addClass,
topRightBottomLeft,
TRBL,
equalTRBL,
optionsTemplateTypes as oTypes,
OptionsTemplateValue,
style,
hasOwnProperty,
} from 'support';
import { OSTargetObject } from 'typings';
import { createLifecycleBase, Lifecycle } from 'lifecycles/lifecycleBase';
import { getEnvironment, Environment } from 'environment';
import { createSizeObserver } from 'observers/sizeObserver';
import { createTrinsicObserver } from 'observers/trinsicObserver';
export type OverflowBehavior = 'hidden' | 'scroll' | 'visible-hidden' | 'visible-scroll';
export interface StructureLifecycleOptions {
@@ -39,7 +35,11 @@ const classNameViewportScrollbarStyling = `${classNameViewport}-scrollbar-styled
const cssMarginEnd = cssProperty('margin-inline-end');
const cssBorderEnd = cssProperty('border-inline-end');
export const createStructureLifecycle = (target: HTMLElement, initialOptions?: StructureLifecycleOptions): Lifecycle<StructureLifecycleOptions> => {
export const createStructureLifecycle = (
target: OSTargetObject,
initialOptions?: StructureLifecycleOptions
): Lifecycle<StructureLifecycleOptions> => {
const { host, viewport, content } = target;
const destructFns: (() => any)[] = [];
const env: Environment = getEnvironment();
const scrollbarsOverlaid = env._nativeScrollbarIsOverlaid;
@@ -48,10 +48,7 @@ export const createStructureLifecycle = (target: HTMLElement, initialOptions?: S
// 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 viewportElm = createDOM(`<div class="${classNameViewport} ${classNameViewportScrollbarStyling}"></div>`)[0];
const contentElm = createDOM(`<div class="${classNameContent}"></div>`)[0];
const { _options, _update, _cacheChange } = createLifecycleBase<StructureLifecycleOptions, StructureLifecycleCache>(
const { _options, _update, _updateCache } = createLifecycleBase<StructureLifecycleOptions, StructureLifecycleCache>(
{
paddingAbsolute: [false, oTypes.boolean],
overflowBehavior: {
@@ -60,37 +57,58 @@ export const createStructureLifecycle = (target: HTMLElement, initialOptions?: S
},
},
{
padding: [() => topRightBottomLeft(target, 'padding'), equalTRBL],
padding: [() => topRightBottomLeft(host, 'padding'), equalTRBL],
},
initialOptions,
(changedOptions, changedCache) => {
if (hasOwnProperty(changedOptions, 'paddingAbsolute') || hasOwnProperty(changedCache, 'padding')) {
const { padding } = changedCache;
const { paddingAbsolute } = changedOptions;
const paddingStyle: TRBL = {
t: 0,
r: 0,
b: 0,
l: 0,
};
if (!paddingAbsolute) {
paddingStyle.t = -padding!.t;
paddingStyle.r = -(padding!.r + padding!.l);
paddingStyle.b = -(padding!.b + padding!.t);
paddingStyle.l = -padding!.l;
}
if (!supportsScrollbarStyling) {
paddingStyle.r -= env._nativeScrollbarSize.y;
paddingStyle.b -= env._nativeScrollbarSize.x;
}
style(viewport, { top: paddingStyle.t, left: paddingStyle.l, 'margin-right': paddingStyle.r, 'margin-bottom': paddingStyle.b });
}
console.log(changedOptions); // eslint-disable-line
console.log(changedCache); // eslint-disable-line
}
);
// eslint-disable-next-line
const onSizeChanged = (direction?: 'ltr' | 'rtl') => {
_cacheChange('padding');
const onSizeChanged = () => {
_updateCache('padding');
};
const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsic: boolean) => {
console.log('heightAuot', heightIntrinsic); // eslint-disable-line
if (heightIntrinsic) {
style(content, { height: 'auto' });
} else {
style(content, { height: '100%' });
}
};
appendChildren(viewportElm, contentElm);
appendChildren(contentElm, contents(target));
appendChildren(target, viewportElm);
addClass(target, classNameHost);
destructFns.push(createSizeObserver(target, onSizeChanged, { _appear: true, _direction: !directionObserverObsolete }));
destructFns.push(createTrinsicObserver(target, onTrinsicChanged));
return {
_options,
_update,
_onSizeChanged: onSizeChanged,
_onTrinsicChanged: onTrinsicChanged,
_destruct() {
runEach(destructFns);
removeElements(viewportElm);
},
};
};
@@ -118,7 +118,7 @@ export const createSizeObserver = (
height: scrollAmount,
});
reset();
appearCallback = appear ? onScroll : reset;
appearCallback = appear ? () => onScroll() : reset;
}
if (direction) {
@@ -1,54 +1,70 @@
import { validateOptions, assignDeep } from 'support';
import { Options, optionsTemplate } from 'options';
import { TargetElement } from 'overlayscrollbars';
import { Environment } from 'environment';
import { OSTarget, OSTargetObject } from 'typings';
import { createStructureLifecycle } from 'lifecycles/structureLifecycle';
import { appendChildren, addClass, contents, is, isHTMLElement, createDiv, each } from 'support';
import { createSizeObserver } from 'observers/sizeObserver';
import { createTrinsicObserver } from 'observers/trinsicObserver';
import { Lifecycle } from 'lifecycles/lifecycleBase';
let ENVIRONMENT: Environment;
const classNameHost = 'os-host';
const classNameViewport = 'os-viewport';
const classNameContent = 'os-content';
interface UpdateHints {
_changedOptions: Options;
}
const normalizeTarget = (target: OSTarget): OSTargetObject => {
if (isHTMLElement(target)) {
const isTextarea = is(target, 'textarea');
const host = (isTextarea ? createDiv() : target) as HTMLElement;
const viewport = createDiv(classNameViewport);
const content = createDiv(classNameContent);
interface OverlayScrollbarsInstanceVars {
_documentElm: Document;
_windowElm: Window;
_htmlElm: HTMLElement;
_bodyElm: HTMLElement;
_targetElm: TargetElement;
_isTextarea: boolean;
_isBody: boolean;
_currentOptions: Options;
_setOptions(newOptions: Options): Options;
_update(updateHints: UpdateHints): void;
}
/*
const initSingletons = () => {
if (!ENVIRONMENT) {
ENVIRONMENT = new Environment();
appendChildren(viewport, content);
appendChildren(content, contents(target));
appendChildren(target, viewport);
addClass(host, classNameHost);
return {
target,
host,
viewport,
content,
};
}
const { host, viewport, content } = target;
addClass(host, classNameHost);
addClass(viewport, classNameViewport);
addClass(content, classNameContent);
return target;
};
export class OverlayScrollbars {
#instanceVars: OverlayScrollbarsInstanceVars = {
_setOptions(newOptions: Options): Options {
const { _currentOptions } = this;
const { _validated } = validateOptions(newOptions, optionsTemplate, _currentOptions, true);
const OverlayScrollbars = (target: OSTarget, options?: any, extensions?: any): void => {
const osTarget: OSTargetObject = normalizeTarget(target);
const lifecycles: Lifecycle<any>[] = [];
const { host } = osTarget;
this._currentOptions = assignDeep({}, _currentOptions, _validated);
lifecycles.push(createStructureLifecycle(osTarget));
return _validated;
},
// eslint-disable-next-line
const onSizeChanged = (direction?: 'ltr' | 'rtl') => {
if (direction) {
each(lifecycles, (lifecycle) => {
lifecycle._onDirectionChanged && lifecycle._onDirectionChanged(direction);
});
} else {
each(lifecycles, (lifecycle) => {
lifecycle._onSizeChanged && lifecycle._onSizeChanged();
});
}
};
const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsic: boolean) => {
each(lifecycles, (lifecycle) => {
lifecycle._onTrinsicChanged && lifecycle._onTrinsicChanged(widthIntrinsic, heightIntrinsic);
});
};
constructor(target: HTMLElement, options: Options) {
this.#instanceVars._documentElm = document;
this.#instanceVars._windowElm = window;
this.#instanceVars._htmlElm = document.body;
this.#instanceVars._bodyElm = document.body;
this.#instanceVars._targetElm = document.body;
this.#instanceVars._isTextarea = false;
this.#instanceVars._isBody = false;
initSingletons();
}
}
*/
createSizeObserver(host, onSizeChanged, { _appear: true, _direction: true });
createTrinsicObserver(host, onTrinsicChanged);
};
export { OverlayScrollbars };
@@ -1,11 +1,18 @@
import { each } from 'support/utils/array';
import { attr } from 'support/dom/attribute';
import { contents } from 'support/dom/traversal';
import { removeElements } from 'support/dom/manipulation';
/**
* Creates a div DOM node.
*/
export const createDiv = (): HTMLDivElement => document.createElement('div');
export const createDiv = (classNames?: string): HTMLDivElement => {
const div = document.createElement('div');
if (classNames) {
attr(div, 'class', classNames);
}
return div;
};
/**
* Creates DOM nodes modeled after the passed html string and returns the root dom nodes as a array.
@@ -95,10 +95,10 @@ export const show = (elm: HTMLElement | null): void => {
*/
export const topRightBottomLeft = (elm: HTMLElement | null, property?: string): TRBL => {
const finalProp = property || '';
const top = `${finalProp}Top`;
const right = `${finalProp}Right`;
const bottom = `${finalProp}Bottom`;
const left = `${finalProp}Left`;
const top = `${finalProp}-top`;
const right = `${finalProp}-right`;
const bottom = `${finalProp}-bottom`;
const left = `${finalProp}-left`;
const result = style(elm, [top, right, bottom, left]);
return {
t: parseToZeroOrNumber(result[top]),
@@ -1,5 +1,15 @@
import { each, from } from 'support/utils/array';
const matches = (elm: Element | null, selector: string): boolean => {
if (elm) {
// eslint-disable-next-line
// @ts-ignore
const fn = Element.prototype.matches || Element.prototype.msMatchesSelector;
return fn.call(elm, selector);
}
return false;
};
/**
* Find all elements with the passed selector, outgoing (and including) the passed element or the document if no element was provided.
* @param selector The selector which has to be searched by.
@@ -27,7 +37,7 @@ export const findFirst = (selector: string, elm?: Element | null): Element | nul
* @param elm The element which has to be compared with the passed selector.
* @param selector The selector which has to be compared with the passed element. Additional selectors: ':visible' and ':hidden'.
*/
export const is = (elm: Element | null, selector: string): boolean => (elm ? elm.matches(selector) : false);
export const is = (elm: Element | null, selector: string): boolean => matches(elm, selector);
/**
* Returns the children (no text-nodes or comments) of the passed element which are matching the passed selector. An empty array is returned if the passed element is null.
@@ -39,7 +49,7 @@ export const children = (elm: Element | null, selector?: string): ReadonlyArray<
each(elm && elm.children, (child: Element) => {
if (selector) {
if (child.matches(selector)) {
if (matches(child, selector)) {
childs.push(child);
}
} else {
+11
View File
@@ -1,5 +1,16 @@
export type PlainObject<T = any> = { [name: string]: T };
export type OSTargetElement = HTMLElement | HTMLTextAreaElement;
export interface OSTargetObject {
target: OSTargetElement;
host: HTMLElement;
viewport: HTMLElement;
content: HTMLElement;
}
export type OSTarget = OSTargetElement | OSTargetObject;
/*
export namespace OverlayScrollbars {
export type ResizeBehavior = 'none' | 'both' | 'horizontal' | 'vertical';
@@ -102,29 +102,29 @@ describe('lifecycleBase', () => {
describe('cache', () => {
test('single value cache change', () => {
const updateFn = jest.fn();
const { _cacheChange } = createLifecycle({}, updateFn);
const { _updateCache } = createLifecycle({}, updateFn);
_cacheChange('number');
_updateCache('number');
expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({}, { number: 2 });
_cacheChange('constant');
_updateCache('constant');
expect(updateFn).toBeCalledTimes(2);
});
test('multiple value cache change', () => {
const updateFn = jest.fn();
const { _cacheChange } = createLifecycle({}, updateFn);
const { _updateCache } = createLifecycle({}, updateFn);
_cacheChange(['number', 'object']);
_updateCache(['number', 'object']);
expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({}, { number: 2, object: { string: 'hihi', boolean: false } });
_cacheChange(['number', 'constant']);
_updateCache(['number', 'constant']);
expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toBeCalledWith({}, { number: 3 });
_cacheChange(['constant']);
_updateCache(['constant']);
expect(updateFn).toBeCalledTimes(3);
});
});
@@ -173,17 +173,17 @@ describe('lifecycleBase', () => {
test('updates correctly on cache change', () => {
const updateFn = jest.fn();
const { _cacheChange } = createLifecycle({}, updateFn);
const { _updateCache } = createLifecycle({}, updateFn);
_cacheChange('number');
_updateCache('number');
expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({}, { number: 2 });
_cacheChange(['number', 'object', 'constant']);
_updateCache(['number', 'object', 'constant']);
expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toBeCalledWith({}, { number: 3, object: { string: 'hihi', boolean: false } });
_cacheChange('constant');
_updateCache('constant');
expect(updateFn).toBeCalledTimes(3);
});
@@ -30,6 +30,11 @@ describe('dom create', () => {
const createdDiv = createDiv();
expect(createdDiv.parentElement).toBe(null);
});
test('with class names', () => {
const createdDiv = createDiv('a b c');
expect(createdDiv.classList.length).toBe(3);
});
});
describe('createDOM', () => {
@@ -1,7 +1,6 @@
import 'overlayscrollbars.scss';
import './index.scss';
import { createStructureLifecycle } from 'lifecycles/structureLifecycle';
import { OverlayScrollbars } from 'overlayscrollbars/OverlayScrollbars';
const targetElm = document.querySelector('#target') as HTMLElement;
const structureLifecycle = createStructureLifecycle(targetElm);
OverlayScrollbars(targetElm);
@@ -137,6 +137,11 @@ const iterateDirection = async (afterEach?: () => any) => {
const start = async () => {
setTestResult(null);
console.log('init direction changes:', directionIterations);
console.log('init size changes:', sizeIterations);
should.ok(directionIterations > 0);
should.ok(sizeIterations > 0);
targetElm?.removeAttribute('style');
await iterateDisplay();
await iterateDirection();