mirror of
https://github.com/tenrok/OverlayScrollbars.git
synced 2026-06-18 21:10:38 +03:00
wip
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
+2
-3
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user