add eventing

This commit is contained in:
Rene Haas
2022-06-23 17:44:15 +02:00
parent 4d6f2914eb
commit 8f3aa1c93a
11 changed files with 206 additions and 148 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ module.exports = {
}, },
], ],
'import/extensions': [ 'import/extensions': [
'error', 'off',
'ignorePackages', 'ignorePackages',
{ {
js: 'never', js: 'never',
@@ -0,0 +1,110 @@
import { OSOptions } from 'options';
import { each, from, isArray, keys } from 'support';
import { PartialOptions } from 'typings';
/*
onScrollStart : null,
onScroll : null,
onScrollStop : null,
onOverflowChanged : null,
onOverflowAmountChanged : null,
onDirectionChanged : null,
onContentSizeChanged : null,
onHostSizeChanged : null,
*/
export interface OnUpdatedEventListenerArgs {
updateHints: {
sizeChanged: boolean;
hostMutation: boolean;
contentMutation: boolean;
directionChanged: boolean;
heightIntrinsicChanged: boolean;
};
changedOptions: PartialOptions<OSOptions>;
force: boolean;
}
export interface EventListenerArgsMap {
initialized: false;
initializationWithdrawn: false;
destroyed: false;
updated: OnUpdatedEventListenerArgs;
}
export type OSEventListener<N extends keyof EventListenerArgsMap> = (
args: EventListenerArgsMap[N]
) => void;
export type AddEventListener = <N extends keyof EventListenerArgsMap>(
name: N,
listener: OSEventListener<N> | OSEventListener<N>[]
) => () => void;
export type RemoveEventListener = <N extends keyof EventListenerArgsMap>(
name?: N,
listener?: OSEventListener<N> | OSEventListener<N>[]
) => void;
export type TriggerEventListener = <N extends keyof EventListenerArgsMap>(
name: N,
args: EventListenerArgsMap[N]
) => void;
export type EventListenersHub = [AddEventListener, RemoveEventListener, TriggerEventListener];
export type EventListenersMap = {
[K in keyof EventListenerArgsMap]?: OSEventListener<K> | OSEventListener<K>[];
};
const manageListener = <N extends keyof EventListenerArgsMap>(
callback: (listener?: OSEventListener<any>) => void,
listener?: OSEventListener<N> | OSEventListener<N>[]
) => {
each(isArray(listener) ? listener : [listener], callback);
};
export const createEventListenerHub = (
initialEventListeners?: EventListenersMap
): EventListenersHub => {
const events = new Map<string, Set<OSEventListener<any>>>();
const removeEvent: RemoveEventListener = (name?, listener?) => {
if (name) {
const eventSet = events.get(name);
manageListener((currListener) => {
if (eventSet) {
eventSet[currListener ? 'delete' : 'clear'](currListener!);
}
}, listener);
} else {
events.forEach((eventSet) => {
eventSet.clear();
});
events.clear();
}
};
const addEvent: AddEventListener = (name, listener) => {
const eventSet = events.get(name) || new Set();
events.set(name, eventSet);
manageListener((currListener) => {
eventSet.add(currListener!);
}, listener);
return removeEvent.bind(0, name, listener as any);
};
const triggerEvent: TriggerEventListener = (name, args) => {
const eventSet = events.get(name);
each(from(eventSet), (event) => {
args ? event(args) : (event as () => void)();
});
};
const initialListenerKeys = keys(initialEventListeners) as (keyof EventListenerArgsMap)[];
each(initialListenerKeys, (key) => {
addEvent(key, initialEventListeners![key] as any);
});
return [addEvent, removeEvent, triggerEvent];
};
-81
View File
@@ -1,81 +0,0 @@
import { OSOptions } from 'options';
import { each, from, isArray } from 'support';
import { PartialOptions } from 'typings';
export interface onUpdatedEventArgs {
updateHints: {
sizeChanged: boolean;
hostMutation: boolean;
contentMutation: boolean;
directionChanged: boolean;
heightIntrinsicChanged: boolean;
};
changedOptions: PartialOptions<OSOptions>;
force: boolean;
}
export interface EventArgsMap {
updated: onUpdatedEventArgs;
}
export type OSEventListener<N extends keyof EventArgsMap> = (args: EventArgsMap[N]) => void;
export type AddEvent = <N extends keyof EventArgsMap>(
name: N,
listener: OSEventListener<N> | OSEventListener<N>[]
) => () => void;
export type RemoveEvent = <N extends keyof EventArgsMap>(
name?: N,
listener?: OSEventListener<N> | OSEventListener<N>[]
) => void;
export type TriggerEvent = <N extends keyof EventArgsMap>(name: N, args: EventArgsMap[N]) => void;
export type EventHub = [AddEvent, RemoveEvent, TriggerEvent];
const manageListener = <N extends keyof EventArgsMap>(
callback: (listener?: OSEventListener<any>) => void,
listener?: OSEventListener<N> | OSEventListener<N>[]
) => {
each(isArray(listener) ? listener : [listener], callback);
};
export const createEventHub = (): EventHub => {
const events = new Map<string, Set<OSEventListener<any>>>();
const removeEvent: RemoveEvent = (name?, listener?) => {
if (name) {
const eventSet = events.get(name);
manageListener((currListener) => {
if (eventSet) {
eventSet[currListener ? 'delete' : 'clear'](currListener!);
}
}, listener);
} else {
events.forEach((eventSet) => {
eventSet.clear();
});
events.clear();
}
};
const addEvent: AddEvent = (name, listener) => {
const eventSet = events.get(name) || new Set();
events.set(name, eventSet);
manageListener((currListener) => {
eventSet.add(currListener!);
}, listener);
return removeEvent.bind(0, name, listener as any);
};
const triggerEvent: TriggerEvent = (name, args) => {
const eventSet = events.get(name);
each(from(eventSet), (event) => {
event(args);
});
};
return [addEvent, removeEvent, triggerEvent];
};
@@ -13,13 +13,15 @@ import {
import { OSOptions } from 'options'; import { OSOptions } from 'options';
import { getEnvironment } from 'environment'; import { getEnvironment } from 'environment';
import { StructureSetup } from 'setups/structureSetup'; import { StructureSetup } from 'setups/structureSetup';
import { lifecycleHubOservers } from 'lifecycles/lifecycleHubObservers'; import { lifecycleHubOservers, UpdateObserverOptions } from 'lifecycles/lifecycleHubObservers';
import { createTrinsicLifecycle } from 'lifecycles/trinsicLifecycle'; import { createTrinsicLifecycle } from 'lifecycles/trinsicLifecycle';
import { createPaddingLifecycle } from 'lifecycles/paddingLifecycle'; import { createPaddingLifecycle } from 'lifecycles/paddingLifecycle';
import { createOverflowLifecycle } from 'lifecycles/overflowLifecycle'; import { createOverflowLifecycle } from 'lifecycles/overflowLifecycle';
import { StyleObject, PartialOptions } from 'typings'; import { StyleObject, PartialOptions } from 'typings';
import { ScrollbarsSetup } from 'setups/scrollbarsSetup'; import { ScrollbarsSetup } from 'setups/scrollbarsSetup';
import { TriggerEvent } from '../events'; import { TriggerEventListener } from 'eventListeners';
import { SizeObserver } from 'observers/sizeObserver';
import { TrinsicObserver } from 'observers/trinsicObserver';
export type LifecycleCheckOption = <T>(path: string) => LifecycleOptionInfo<T>; export type LifecycleCheckOption = <T>(path: string) => LifecycleOptionInfo<T>;
@@ -109,11 +111,15 @@ const lifecycleCommunicationFallback: LifecycleCommunication = {
export const createLifecycleHub = ( export const createLifecycleHub = (
options: OSOptions, options: OSOptions,
triggerEvent: TriggerEvent, triggerListener: TriggerEventListener,
structureSetup: StructureSetup, structureSetup: StructureSetup,
scrollbarsSetup: ScrollbarsSetup scrollbarsSetup: ScrollbarsSetup
): LifecycleHubInstance => { ): LifecycleHubInstance => {
let lifecycleCommunication = lifecycleCommunicationFallback; let lifecycleCommunication = lifecycleCommunicationFallback;
let sizeObserver: SizeObserver;
let trinsicObserver: false | TrinsicObserver;
let updateObserverOptions: UpdateObserverOptions;
let destroyObservers: () => void;
const { _viewport } = structureSetup._targetObj; const { _viewport } = structureSetup._targetObj;
const { const {
_nativeScrollbarStyling, _nativeScrollbarStyling,
@@ -157,13 +163,13 @@ export const createLifecycleHub = (
const finalDirectionIsRTL = const finalDirectionIsRTL =
_directionIsRTL || _directionIsRTL ||
(_sizeObserver (sizeObserver
? _sizeObserver._getCurrentCacheValues(force)._directionIsRTL ? sizeObserver._getCurrentCacheValues(force)._directionIsRTL
: booleanCacheValuesFallback); : booleanCacheValuesFallback);
const finalHeightIntrinsic = const finalHeightIntrinsic =
_heightIntrinsic || _heightIntrinsic ||
(_trinsicObserver (trinsicObserver
? _trinsicObserver._getCurrentCacheValues(force)._heightIntrinsic ? trinsicObserver._getCurrentCacheValues(force)._heightIntrinsic
: booleanCacheValuesFallback); : booleanCacheValuesFallback);
const checkOption: LifecycleCheckOption = (path) => [ const checkOption: LifecycleCheckOption = (path) => [
getPropByPath(options, path), getPropByPath(options, path),
@@ -174,8 +180,8 @@ export const createLifecycleHub = (
const scrollOffsetY = adjustScrollOffset && scrollTop(_viewport); const scrollOffsetY = adjustScrollOffset && scrollTop(_viewport);
// place before updating lifecycles because of possible flushing of debounce // place before updating lifecycles because of possible flushing of debounce
if (_updateObserverOptions) { if (updateObserverOptions) {
_updateObserverOptions(checkOption); updateObserverOptions(checkOption);
} }
each(lifecycles, (lifecycle) => { each(lifecycles, (lifecycle) => {
@@ -210,7 +216,7 @@ export const createLifecycleHub = (
scrollTop(_viewport, scrollOffsetY); scrollTop(_viewport, scrollOffsetY);
} }
triggerEvent('updated', { triggerListener('updated', {
updateHints: { updateHints: {
sizeChanged: _sizeChanged, sizeChanged: _sizeChanged,
contentMutation: _contentMutation, contentMutation: _contentMutation,
@@ -222,12 +228,11 @@ export const createLifecycleHub = (
force: !!force, force: !!force,
}); });
}; };
const { // eslint-disable-next-line prefer-const
_sizeObserver, [sizeObserver, trinsicObserver, updateObserverOptions, destroyObservers] = lifecycleHubOservers(
_trinsicObserver, instance,
_updateObserverOptions, updateLifecycles
_destroy: destroyObservers, );
} = lifecycleHubOservers(instance, updateLifecycles);
const update = (changedOptions: Partial<OSOptions>, force?: boolean) => const update = (changedOptions: Partial<OSOptions>, force?: boolean) =>
updateLifecycles({}, changedOptions, force); updateLifecycles({}, changedOptions, force);
@@ -1,5 +1,4 @@
import { import {
CacheValues,
diffClass, diffClass,
debounce, debounce,
isArray, isArray,
@@ -9,13 +8,27 @@ import {
isString, isString,
attr, attr,
removeAttr, removeAttr,
CacheValues,
} from 'support'; } from 'support';
import { getEnvironment } from 'environment'; import { getEnvironment } from 'environment';
import { createSizeObserver, SizeObserverCallbackParams } from 'observers/sizeObserver'; import {
import { createTrinsicObserver } from 'observers/trinsicObserver'; createSizeObserver,
SizeObserver,
SizeObserverCallbackParams,
} from 'observers/sizeObserver';
import { createTrinsicObserver, TrinsicObserver } from 'observers/trinsicObserver';
import { createDOMObserver, DOMObserver } from 'observers/domObserver'; import { createDOMObserver, DOMObserver } from 'observers/domObserver';
import { LifecycleHub, LifecycleCheckOption, LifecycleUpdateHints } from 'lifecycles/lifecycleHub'; import { LifecycleHub, LifecycleCheckOption, LifecycleUpdateHints } from 'lifecycles/lifecycleHub';
export type UpdateObserverOptions = (checkOption: LifecycleCheckOption) => void;
export type LifecycleHubObservers = [
SizeObserver,
TrinsicObserver | false,
UpdateObserverOptions,
() => void
];
// const hostSelector = `.${classNameHost}`; // const hostSelector = `.${classNameHost}`;
// TODO: observer textarea attrs if textarea // TODO: observer textarea attrs if textarea
@@ -45,7 +58,7 @@ const ignoreTargetChange = (
export const lifecycleHubOservers = ( export const lifecycleHubOservers = (
instance: LifecycleHub, instance: LifecycleHub,
updateLifecycles: (updateHints: Partial<LifecycleUpdateHints>) => unknown updateLifecycles: (updateHints: Partial<LifecycleUpdateHints>) => unknown
) => { ): LifecycleHubObservers => {
let debounceTimeout: number | false | undefined; let debounceTimeout: number | false | undefined;
let debounceMaxDelay: number | false | undefined; let debounceMaxDelay: number | false | undefined;
let contentMutationObserver: DOMObserver | undefined; let contentMutationObserver: DOMObserver | undefined;
@@ -145,7 +158,7 @@ export const lifecycleHubOservers = (
_ignoreTargetChange: ignoreTargetChange, _ignoreTargetChange: ignoreTargetChange,
}); });
const updateOptions = (checkOption: LifecycleCheckOption) => { const updateOptions: UpdateObserverOptions = (checkOption) => {
const [elementEvents, elementEventsChanged] = checkOption<Array<[string, string]> | null>( const [elementEvents, elementEventsChanged] = checkOption<Array<[string, string]> | null>(
'updating.elementEvents' 'updating.elementEvents'
); );
@@ -198,15 +211,15 @@ export const lifecycleHubOservers = (
updateViewportAttrsFromHost(); updateViewportAttrsFromHost();
return { return [
_trinsicObserver: trinsicObserver, sizeObserver,
_sizeObserver: sizeObserver, trinsicObserver,
_updateObserverOptions: updateOptions, updateOptions,
_destroy() { () => {
contentMutationObserver && contentMutationObserver._destroy(); contentMutationObserver && contentMutationObserver._destroy();
trinsicObserver && trinsicObserver._destroy(); trinsicObserver && trinsicObserver._destroy();
sizeObserver._destroy(); sizeObserver._destroy();
hostMutationObserver._destroy(); hostMutationObserver._destroy();
}, },
}; ];
}; };
@@ -47,9 +47,7 @@ export interface SizeObserverCallbackParams {
export interface SizeObserver { export interface SizeObserver {
_destroy(): void; _destroy(): void;
_getCurrentCacheValues( _getCurrentCacheValues(force?: boolean): {
force?: boolean
): {
_directionIsRTL: CacheValues<boolean>; _directionIsRTL: CacheValues<boolean>;
}; };
} }
@@ -15,9 +15,7 @@ import { classNameTrinsicObserver } from 'classnames';
export interface TrinsicObserver { export interface TrinsicObserver {
_destroy(): void; _destroy(): void;
_getCurrentCacheValues( _getCurrentCacheValues(force?: boolean): {
force?: boolean
): {
_heightIntrinsic: CacheValues<boolean>; _heightIntrinsic: CacheValues<boolean>;
}; };
} }
@@ -13,13 +13,18 @@ import {
OptionsValidationPluginInstance, OptionsValidationPluginInstance,
} from 'plugins'; } from 'plugins';
import { addInstance, getInstance, removeInstance } from 'instances'; import { addInstance, getInstance, removeInstance } from 'instances';
import { createEventHub, AddEvent, RemoveEvent } from './events'; import {
createEventListenerHub,
EventListenersMap,
AddEventListener,
RemoveEventListener,
} from 'eventListeners';
export interface OverlayScrollbarsStatic { export interface OverlayScrollbarsStatic {
( (
target: OSTarget | OSInitializationObject, target: OSTarget | OSInitializationObject,
options?: PartialOptions<OSOptions>, options?: PartialOptions<OSOptions>,
extensions?: any eventListeners?: EventListenersMap
): OverlayScrollbars; ): OverlayScrollbars;
extend(osPlugin: OSPlugin | OSPlugin[]): void; extend(osPlugin: OSPlugin | OSPlugin[]): void;
@@ -34,22 +39,23 @@ export interface OverlayScrollbars {
state(): any; state(): any;
on: AddEvent; on: AddEventListener;
off: RemoveEvent; off: RemoveEventListener;
} }
export const OverlayScrollbars: OverlayScrollbarsStatic = ( export const OverlayScrollbars: OverlayScrollbarsStatic = (
target: OSTarget | OSInitializationObject, target,
options?: PartialOptions<OSOptions> options?,
eventListeners?
): OverlayScrollbars => { ): OverlayScrollbars => {
const { _getDefaultOptions, _nativeScrollbarIsOverlaid } = getEnvironment();
const plugins = getPlugins();
const instanceTarget = isHTMLElement(target) ? target : target.target; const instanceTarget = isHTMLElement(target) ? target : target.target;
const potentialInstance = getInstance(instanceTarget); const potentialInstance = getInstance(instanceTarget);
if (potentialInstance) { if (potentialInstance) {
return potentialInstance; return potentialInstance;
} }
const { _getDefaultOptions } = getEnvironment();
const plugins = getPlugins();
const optionsValidationPlugin = plugins[ const optionsValidationPlugin = plugins[
optionsValidationPluginName optionsValidationPluginName
] as OptionsValidationPluginInstance; ] as OptionsValidationPluginInstance;
@@ -58,8 +64,17 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
const validate = optionsValidationPlugin && optionsValidationPlugin._; const validate = optionsValidationPlugin && optionsValidationPlugin._;
return validate ? validate(opts, true) : opts; return validate ? validate(opts, true) : opts;
}; };
const [addEvent, removeEvent, triggerEvent] = createEventHub();
const currentOptions: OSOptions = assignDeep({}, _getDefaultOptions(), validateOptions(options)); const currentOptions: OSOptions = assignDeep({}, _getDefaultOptions(), validateOptions(options));
const [addEvent, removeEvent, triggerEvent] = createEventListenerHub(eventListeners);
if (
_nativeScrollbarIsOverlaid.x &&
_nativeScrollbarIsOverlaid.y &&
!currentOptions.nativeScrollbarsOverlaid.initialize
) {
triggerEvent('initializationWithdrawn', false);
}
const structureSetup: StructureSetup = createStructureSetup(target); const structureSetup: StructureSetup = createStructureSetup(target);
const scrollbarsSetup: ScrollbarsSetup = createScrollbarsSetup(target, structureSetup); const scrollbarsSetup: ScrollbarsSetup = createScrollbarsSetup(target, structureSetup);
const lifecycleHub = createLifecycleHub( const lifecycleHub = createLifecycleHub(
@@ -91,6 +106,7 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
lifecycleHub._destroy(); lifecycleHub._destroy();
removeInstance(instanceTarget); removeInstance(instanceTarget);
removeEvent(); removeEvent();
triggerEvent('destroyed', false);
}, },
}; };
@@ -105,6 +121,8 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
addInstance(instanceTarget, instance); addInstance(instanceTarget, instance);
triggerEvent('initialized', false);
return instance; return instance;
}; };
@@ -2,10 +2,6 @@ import 'styles/overlayscrollbars.scss';
import './index.scss'; import './index.scss';
import './handleEnvironment'; import './handleEnvironment';
import should from 'should'; import should from 'should';
import { resize } from '@/testing-browser/Resize';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { generateClassChangeSelectCallback, iterateSelect } from '@/testing-browser/Select';
import { timeout } from '@/testing-browser/timeout';
import { OverlayScrollbars } from 'overlayscrollbars'; import { OverlayScrollbars } from 'overlayscrollbars';
import { import {
assignDeep, assignDeep,
@@ -18,6 +14,10 @@ import {
WH, WH,
removeAttr, removeAttr,
} from 'support'; } from 'support';
import { resize } from '@/testing-browser/Resize';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { generateClassChangeSelectCallback, iterateSelect } from '@/testing-browser/Select';
import { timeout } from '@/testing-browser/timeout';
interface Metrics { interface Metrics {
offset: { offset: {
@@ -62,12 +62,10 @@ const getMetrics = (elm: HTMLElement): Metrics => {
const comparisonEndBCR = getBoundingClientRect(elm!.querySelector('.end')!); const comparisonEndBCR = getBoundingClientRect(elm!.querySelector('.end')!);
const targetViewport = target!.querySelector<HTMLElement>('.os-viewport'); const targetViewport = target!.querySelector<HTMLElement>('.os-viewport');
const scrollMeasure = (elm: HTMLElement) => { const scrollMeasure = (elm: HTMLElement) => ({
return {
width: Math.max(0, elm!.scrollWidth - elm!.clientWidth), width: Math.max(0, elm!.scrollWidth - elm!.clientWidth),
height: Math.max(0, elm!.scrollHeight - elm!.clientHeight), height: Math.max(0, elm!.scrollHeight - elm!.clientHeight),
}; });
};
const hasOverflow = (elm: HTMLElement) => { const hasOverflow = (elm: HTMLElement) => {
const measure = scrollMeasure(elm); const measure = scrollMeasure(elm);
@@ -155,11 +153,13 @@ if (!useContentElement) {
let updateCount = 0; let updateCount = 0;
// @ts-ignore // @ts-ignore
const osInstance = (window.os = OverlayScrollbars( const osInstance =
// @ts-ignore
(window.os = OverlayScrollbars(
{ target: target!, content: useContentElement }, { target: target!, content: useContentElement },
{ nativeScrollbarsOverlaid: { initialize: true } },
{ {
callbacks: { updated() {
onUpdated() {
updateCount++; updateCount++;
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (targetUpdatesSlot) { if (targetUpdatesSlot) {
@@ -167,9 +167,8 @@ const osInstance = (window.os = OverlayScrollbars(
} }
}); });
}, },
},
} }
)); ));
target!.querySelector('.os-viewport')?.addEventListener('scroll', (e) => { target!.querySelector('.os-viewport')?.addEventListener('scroll', (e) => {
const viewport: HTMLElement | null = e.currentTarget as HTMLElement; const viewport: HTMLElement | null = e.currentTarget as HTMLElement;
@@ -2,6 +2,8 @@ import 'styles/overlayscrollbars.scss';
import './index.scss'; import './index.scss';
import './handleEnvironment'; import './handleEnvironment';
import should from 'should'; import should from 'should';
import { hasDimensions, offsetSize, WH, style } from 'support';
import { createSizeObserver } from 'observers/sizeObserver';
import { import {
generateClassChangeSelectCallback, generateClassChangeSelectCallback,
iterateSelect, iterateSelect,
@@ -9,9 +11,6 @@ import {
} from '@/testing-browser/Select'; } from '@/testing-browser/Select';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult'; import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { timeout } from '@/testing-browser/timeout'; import { timeout } from '@/testing-browser/timeout';
import { hasDimensions, offsetSize, WH, style } from 'support';
import { createSizeObserver } from 'observers/sizeObserver';
let sizeIterations = 0; let sizeIterations = 0;
let directionIterations = 0; let directionIterations = 0;
@@ -2,6 +2,8 @@ import 'styles/overlayscrollbars.scss';
import './index.scss'; import './index.scss';
import './handleEnvironment'; import './handleEnvironment';
import should from 'should'; import should from 'should';
import { offsetSize } from 'support';
import { createTrinsicObserver } from 'observers/trinsicObserver';
import { import {
generateClassChangeSelectCallback, generateClassChangeSelectCallback,
iterateSelect, iterateSelect,
@@ -9,9 +11,6 @@ import {
} from '@/testing-browser/Select'; } from '@/testing-browser/Select';
import { timeout } from '@/testing-browser/timeout'; import { timeout } from '@/testing-browser/timeout';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult'; import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { offsetSize } from 'support';
import { createTrinsicObserver } from 'observers/trinsicObserver';
let heightIntrinsic: boolean | undefined; let heightIntrinsic: boolean | undefined;
let heightIterations = 0; let heightIterations = 0;