diff --git a/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts b/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts index a03a726..86f6749 100644 --- a/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts +++ b/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts @@ -26,10 +26,13 @@ interface AbstractLifecycle { export interface Lifecycle extends AbstractLifecycle { _destruct(): void; + _onSizeChanged?(): void; + _onDirectionChanged?(direction: 'ltr' | 'rtl'): void; + _onTrinsicChanged?(widthIntrinsic: boolean, heightIntrinsic: boolean): void; } export interface LifecycleBase extends AbstractLifecycle { - _cacheChange(cachePropsToUpdate?: CachePropsToUpdate): void; + _updateCache(cachePropsToUpdate?: CachePropsToUpdate): void; } /** @@ -80,7 +83,7 @@ export const createLifecycleBase = ( _update: (force?: boolean) => { update({ _force: !!force }); }, - _cacheChange: (cachePropsToUpdate?: CachePropsToUpdate) => { + _updateCache: (cachePropsToUpdate?: CachePropsToUpdate) => { update({ _changedCache: cachePropsToUpdate }); }, }; diff --git a/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts b/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts index e70e328..32c9514 100644 --- a/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts +++ b/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts @@ -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 => { +export const createStructureLifecycle = ( + target: OSTargetObject, + initialOptions?: StructureLifecycleOptions +): Lifecycle => { + 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(`
`)[0]; - const contentElm = createDOM(`
`)[0]; - - const { _options, _update, _cacheChange } = createLifecycleBase( + const { _options, _update, _updateCache } = createLifecycleBase( { 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); }, }; }; diff --git a/packages/overlayscrollbars/src/observers/sizeObserver.ts b/packages/overlayscrollbars/src/observers/sizeObserver.ts index 3c4ac5c..6ba85cf 100644 --- a/packages/overlayscrollbars/src/observers/sizeObserver.ts +++ b/packages/overlayscrollbars/src/observers/sizeObserver.ts @@ -118,7 +118,7 @@ export const createSizeObserver = ( height: scrollAmount, }); reset(); - appearCallback = appear ? onScroll : reset; + appearCallback = appear ? () => onScroll() : reset; } if (direction) { diff --git a/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts b/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts index 8312535..723f1d7 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars/OverlayScrollbars.ts @@ -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[] = []; + 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 }; diff --git a/packages/overlayscrollbars/src/support/dom/create.ts b/packages/overlayscrollbars/src/support/dom/create.ts index 3a704ca..57b15b4 100644 --- a/packages/overlayscrollbars/src/support/dom/create.ts +++ b/packages/overlayscrollbars/src/support/dom/create.ts @@ -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. diff --git a/packages/overlayscrollbars/src/support/dom/style.ts b/packages/overlayscrollbars/src/support/dom/style.ts index aa8149a..f3c0f73 100644 --- a/packages/overlayscrollbars/src/support/dom/style.ts +++ b/packages/overlayscrollbars/src/support/dom/style.ts @@ -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]), diff --git a/packages/overlayscrollbars/src/support/dom/traversal.ts b/packages/overlayscrollbars/src/support/dom/traversal.ts index c2c37a7..ff093cb 100644 --- a/packages/overlayscrollbars/src/support/dom/traversal.ts +++ b/packages/overlayscrollbars/src/support/dom/traversal.ts @@ -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 { diff --git a/packages/overlayscrollbars/src/typings.ts b/packages/overlayscrollbars/src/typings.ts index 74f20e0..341fa98 100644 --- a/packages/overlayscrollbars/src/typings.ts +++ b/packages/overlayscrollbars/src/typings.ts @@ -1,5 +1,16 @@ export type PlainObject = { [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'; diff --git a/packages/overlayscrollbars/tests/jsdom/lifecycles/lifecycleBase.test.ts b/packages/overlayscrollbars/tests/jsdom/lifecycles/lifecycleBase.test.ts index 8c529c7..32f7a9e 100644 --- a/packages/overlayscrollbars/tests/jsdom/lifecycles/lifecycleBase.test.ts +++ b/packages/overlayscrollbars/tests/jsdom/lifecycles/lifecycleBase.test.ts @@ -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); }); diff --git a/packages/overlayscrollbars/tests/jsdom/support/dom/create.test.ts b/packages/overlayscrollbars/tests/jsdom/support/dom/create.test.ts index 1890d80..5abba95 100644 --- a/packages/overlayscrollbars/tests/jsdom/support/dom/create.test.ts +++ b/packages/overlayscrollbars/tests/jsdom/support/dom/create.test.ts @@ -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', () => { diff --git a/packages/overlayscrollbars/tests/puppeteer/lifecycles/structureLifecycle/index.browser.ts b/packages/overlayscrollbars/tests/puppeteer/lifecycles/structureLifecycle/index.browser.ts index ea88a83..5a7eccb 100644 --- a/packages/overlayscrollbars/tests/puppeteer/lifecycles/structureLifecycle/index.browser.ts +++ b/packages/overlayscrollbars/tests/puppeteer/lifecycles/structureLifecycle/index.browser.ts @@ -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); diff --git a/packages/overlayscrollbars/tests/puppeteer/observers/sizeObserver/index.browser.ts b/packages/overlayscrollbars/tests/puppeteer/observers/sizeObserver/index.browser.ts index 43d87fc..2230e84 100644 --- a/packages/overlayscrollbars/tests/puppeteer/observers/sizeObserver/index.browser.ts +++ b/packages/overlayscrollbars/tests/puppeteer/observers/sizeObserver/index.browser.ts @@ -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(); diff --git a/rollup.config.base.js b/rollup.config.base.js index 010f18d..1d85c34 100644 --- a/rollup.config.base.js +++ b/rollup.config.base.js @@ -27,7 +27,7 @@ const rollupConfigDefaults = { }; const legacyBabelConfig = { - exclude: isTestEnv ? [/\/core-js\//, /\/@testing-library\//] : [], + exclude: isTestEnv ? [/\/core-js\//] : [], // /\/@testing-library\// presets: [ [ '@babel/preset-env',