From 7e1aa66163e4e86a813781bc923012dd8c9d2cca Mon Sep 17 00:00:00 2001 From: Denis Hilt Date: Mon, 22 Nov 2021 04:53:35 +0300 Subject: [PATCH 1/3] issue-29 Viewport cleanup --- src/classes/domRoutines.ts | 167 +++++++++++++++++++++---------------- src/classes/logger.ts | 6 +- src/classes/paddings.ts | 10 +-- src/classes/viewport.ts | 37 ++------ src/interfaces/index.ts | 2 + src/scroller.ts | 6 +- src/workflow.ts | 4 +- 7 files changed, 117 insertions(+), 115 deletions(-) diff --git a/src/classes/domRoutines.ts b/src/classes/domRoutines.ts index 89048510..a0843afa 100644 --- a/src/classes/domRoutines.ts +++ b/src/classes/domRoutines.ts @@ -3,14 +3,23 @@ import { Direction } from '../inputs/index'; export class Routines { - readonly horizontal: boolean; - readonly window: boolean; - readonly viewport: HTMLElement | null; - - constructor(settings: Settings) { - this.horizontal = settings.horizontal; - this.window = settings.windowViewport; - this.viewport = settings.viewport; + readonly settings: { + viewport: HTMLElement | null; + horizontal: boolean; + window: boolean; + }; + readonly element: HTMLElement; + readonly viewport: HTMLElement; + + constructor(element: HTMLElement, settings: Settings) { + this.settings = { + viewport: settings.viewport, + horizontal: settings.horizontal, + window: settings.windowViewport + }; + this.element = element; + this.viewport = this.getViewportElement(); + this.onInit(settings); } checkElement(element: HTMLElement): void { @@ -19,117 +28,125 @@ export class Routines { } } - getHostElement(element: HTMLElement): HTMLElement { - if (this.window) { + getViewportElement(): HTMLElement { + if (this.settings.window) { return document.documentElement; } - if (this.viewport) { - return this.viewport; + if (this.settings.viewport) { + return this.settings.viewport; } - this.checkElement(element); - const parent = element.parentElement as HTMLElement; + this.checkElement(this.element); + const parent = this.element.parentElement as HTMLElement; this.checkElement(parent); return parent; } - getScrollEventReceiver(element: HTMLElement): HTMLElement | Window { - if (this.window) { - return window; + onInit(settings: Settings): void { + if (settings.windowViewport) { + if ('scrollRestoration' in history) { + history.scrollRestoration = 'manual'; + } } - return this.getHostElement(element); - } - - setupScrollRestoration(): void { - if ('scrollRestoration' in history) { - history.scrollRestoration = 'manual'; + if (settings.dismissOverflowAnchor) { + this.viewport.style.overflowAnchor = 'none'; } } - dismissOverflowAnchor(element: HTMLElement): void { - this.checkElement(element); - element.style.overflowAnchor = 'none'; - } - findElementBySelector(element: HTMLElement, selector: string): HTMLElement | null { this.checkElement(element); return element.querySelector(selector); } - findPaddingElement(element: HTMLElement, direction: Direction): HTMLElement | null { - return this.findElementBySelector(element, `[data-padding-${direction}]`); + findPaddingElement(direction: Direction): HTMLElement | null { + return this.findElementBySelector(this.element, `[data-padding-${direction}]`); } - findItemElement(element: HTMLElement, id: string): HTMLElement | null { - return this.findElementBySelector(element, `[data-sid="${id}"]`); + findItemElement(id: string): HTMLElement | null { + return this.findElementBySelector(this.element, `[data-sid="${id}"]`); } - getScrollPosition(element: HTMLElement): number { - if (this.window) { - return window.pageYOffset; + getScrollPosition(): number { + if (this.settings.window) { + return this.settings.horizontal ? window.pageXOffset : window.pageYOffset; } - this.checkElement(element); - return element[this.horizontal ? 'scrollLeft' : 'scrollTop']; + return this.viewport[this.settings.horizontal ? 'scrollLeft' : 'scrollTop']; } - setScrollPosition(element: HTMLElement, value: number): void { + setScrollPosition(value: number): void { value = Math.max(0, value); - if (this.window) { - if (this.horizontal) { + if (this.settings.window) { + if (this.settings.horizontal) { window.scrollTo(value, window.scrollY); } else { window.scrollTo(window.scrollX, value); } return; } - this.checkElement(element); - element[this.horizontal ? 'scrollLeft' : 'scrollTop'] = value; + this.viewport[this.settings.horizontal ? 'scrollLeft' : 'scrollTop'] = value; } - getParams(element: HTMLElement, doNotBind?: boolean): DOMRect { + getElementParams(element: HTMLElement): DOMRect { this.checkElement(element); - if (this.window && doNotBind) { - const { clientWidth, clientHeight, clientLeft, clientTop } = element; - return { - 'height': clientHeight, - 'width': clientWidth, - 'top': clientTop, - 'bottom': clientTop + clientHeight, - 'left': clientLeft, - 'right': clientLeft + clientWidth, - 'x': clientLeft, - 'y': clientTop, - 'toJSON': () => null, - }; - } return element.getBoundingClientRect(); } - getSize(element: HTMLElement, doNotBind?: boolean): number { - return this.getParams(element, doNotBind)[this.horizontal ? 'width' : 'height']; + getWindowParams(): DOMRect { + const { clientWidth, clientHeight, clientLeft, clientTop } = this.viewport; + return { + 'height': clientHeight, + 'width': clientWidth, + 'top': clientTop, + 'bottom': clientTop + clientHeight, + 'left': clientLeft, + 'right': clientLeft + clientWidth, + 'x': clientLeft, + 'y': clientTop, + 'toJSON': () => null, + }; + } + + getSize(element: HTMLElement): number { + return this.getElementParams(element)[this.settings.horizontal ? 'width' : 'height']; + } + + getScrollerSize(): number { + return this.getElementParams(this.element)[this.settings.horizontal ? 'width' : 'height']; + } + + getViewportSize(): number { + if (this.settings.window) { + return this.getWindowParams()[this.settings.horizontal ? 'width' : 'height']; + } + return this.getSize(this.viewport); } getSizeStyle(element: HTMLElement): number { this.checkElement(element); - const size = element.style[this.horizontal ? 'width' : 'height']; + const size = element.style[this.settings.horizontal ? 'width' : 'height']; return parseFloat(size as string) || 0; } setSizeStyle(element: HTMLElement, value: number): void { this.checkElement(element); value = Math.max(0, Math.round(value)); - element.style[this.horizontal ? 'width' : 'height'] = `${value}px`; + element.style[this.settings.horizontal ? 'width' : 'height'] = `${value}px`; } - getEdge(element: HTMLElement, direction: Direction, doNotBind?: boolean): number { - const params = this.getParams(element, doNotBind); + getEdge(element: HTMLElement, direction: Direction): number { + const { horizontal } = this.settings; + const params = this.getElementParams(element); const isFwd = direction === Direction.forward; - return params[isFwd ? (this.horizontal ? 'right' : 'bottom') : (this.horizontal ? 'left' : 'top')]; + return params[isFwd ? (horizontal ? 'right' : 'bottom') : (horizontal ? 'left' : 'top')]; } - getEdge2(element: HTMLElement, direction: Direction, relativeElement: HTMLElement, opposite: boolean): number { - // vertical only ? - return element.offsetTop - (relativeElement ? relativeElement.scrollTop : 0) + - (direction === (!opposite ? Direction.forward : Direction.backward) ? this.getSize(element) : 0); + getViewportEdge(direction: Direction): number { + const { window, horizontal } = this.settings; + if (window) { + const params = this.getWindowParams(); + const isFwd = direction === Direction.forward; + return params[isFwd ? (horizontal ? 'right' : 'bottom') : (horizontal ? 'left' : 'top')]; + } + return this.getEdge(this.viewport, direction); } makeElementVisible(element: HTMLElement): void { @@ -144,9 +161,10 @@ export class Routines { element.style.display = 'none'; } - getOffset(element: HTMLElement): number { - this.checkElement(element); - return (this.horizontal ? element.offsetLeft : element.offsetTop) || 0; + getOffset(): number { + const get = (element: HTMLElement) => + (this.settings.horizontal ? element.offsetLeft : element.offsetTop) || 0; + return get(this.element) - (!this.settings.window ? get(this.viewport) : 0); } scrollTo(element: HTMLElement, argument?: boolean | ScrollIntoViewOptions): void { @@ -164,9 +182,10 @@ export class Routines { return () => cancelAnimationFrame(animationFrameId); } - onScroll(element: HTMLElement | Window, handler: EventListener): () => void { - element.addEventListener('scroll', handler); - return () => element.removeEventListener('scroll', handler); + onScroll(handler: EventListener): () => void { + const eventReceiver = this.settings.window ? window : this.viewport; + eventReceiver.addEventListener('scroll', handler); + return () => eventReceiver.removeEventListener('scroll', handler); } } diff --git a/src/classes/logger.ts b/src/classes/logger.ts index 22743f92..40c35779 100644 --- a/src/classes/logger.ts +++ b/src/classes/logger.ts @@ -15,7 +15,7 @@ export class Logger { readonly getWorkflowCycleData: () => string; readonly getLoopId: () => string; readonly getLoopIdNext: () => string; - readonly getScrollPosition: (element: HTMLElement) => number; + readonly getScrollPosition: () => number; private logs: unknown[][] = []; constructor(scroller: Scroller, packageInfo: IPackages, adapter?: { id: number }) { @@ -47,7 +47,7 @@ export class Logger { this.getLoopIdNext = (): string => scroller.state.cycle.loopIdNext; this.getWorkflowCycleData = (): string => `${settings.instanceIndex}-${scroller.state.cycle.count}`; - this.getScrollPosition = (element: HTMLElement) => scroller.routines.getScrollPosition(element); + this.getScrollPosition = () => scroller.routines.getScrollPosition(); this.log(() => 'vscroll Workflow has been started, ' + `core: ${packageInfo.core.name} v${packageInfo.core.version}, ` + @@ -109,7 +109,7 @@ export class Logger { prepareForLog(data: unknown): unknown { return data instanceof Event && data.target - ? this.getScrollPosition(data.target as HTMLElement) + ? this.getScrollPosition() : data; } diff --git a/src/classes/paddings.ts b/src/classes/paddings.ts index 8e6c2288..380a3aa1 100644 --- a/src/classes/paddings.ts +++ b/src/classes/paddings.ts @@ -8,8 +8,8 @@ export class Padding { direction: Direction; routines: Routines; - constructor(element: HTMLElement, direction: Direction, routines: Routines) { - const found = routines.findPaddingElement(element, direction); + constructor(direction: Direction, routines: Routines) { + const found = routines.findPaddingElement(direction); routines.checkElement(found as HTMLElement); this.element = found as HTMLElement; this.direction = direction; @@ -35,10 +35,10 @@ export class Paddings { forward: Padding; backward: Padding; - constructor(element: HTMLElement, routines: Routines, settings: Settings) { + constructor(routines: Routines, settings: Settings) { this.settings = settings; - this.forward = new Padding(element, Direction.forward, routines); - this.backward = new Padding(element, Direction.backward, routines); + this.forward = new Padding(Direction.forward, routines); + this.backward = new Padding(Direction.backward, routines); } byDirection(direction: Direction, opposite?: boolean): Padding { diff --git a/src/classes/viewport.ts b/src/classes/viewport.ts index d114a7b8..afccfa76 100644 --- a/src/classes/viewport.ts +++ b/src/classes/viewport.ts @@ -11,33 +11,17 @@ export class Viewport { offset: number; paddings: Paddings; - readonly element: HTMLElement; readonly settings: Settings; readonly routines: Routines; readonly state: State; readonly logger: Logger; - readonly hostElement: HTMLElement; - readonly scrollEventReceiver: HTMLElement | Window; - - constructor(element: HTMLElement, settings: Settings, routines: Routines, state: State, logger: Logger) { - this.element = element; + constructor(settings: Settings, routines: Routines, state: State, logger: Logger) { this.settings = settings; this.routines = routines; this.state = state; this.logger = logger; - - this.hostElement = this.routines.getHostElement(this.element); - this.scrollEventReceiver = this.routines.getScrollEventReceiver(this.element); - - if (settings.windowViewport) { - this.routines.setupScrollRestoration(); - } - if (settings.dismissOverflowAnchor) { - this.routines.dismissOverflowAnchor(this.hostElement); - } - - this.paddings = new Paddings(this.element, this.routines, settings); + this.paddings = new Paddings(this.routines, settings); } reset(startIndex: number): void { @@ -53,7 +37,7 @@ export class Viewport { this.logger.log(() => ['setting scroll position at', value, '[cancelled]']); return value; } - this.routines.setScrollPosition(this.hostElement, value); + this.routines.setScrollPosition(value); const position = this.scrollPosition; this.logger.log(() => [ 'setting scroll position at', position, ...(position !== value ? [`(${value})`] : []) @@ -62,7 +46,7 @@ export class Viewport { } get scrollPosition(): number { - return this.routines.getScrollPosition(this.hostElement); + return this.routines.getScrollPosition(); } set scrollPosition(value: number) { @@ -70,11 +54,11 @@ export class Viewport { } getSize(): number { - return this.routines.getSize(this.hostElement, true); + return this.routines.getViewportSize(); } getScrollableSize(): number { - return this.routines.getSize(this.element); + return this.routines.getScrollerSize(); } getBufferPadding(): number { @@ -82,18 +66,15 @@ export class Viewport { } getEdge(direction: Direction): number { - return this.routines.getEdge(this.hostElement, direction, true); + return this.routines.getViewportEdge(direction); } setOffset(): void { - this.offset = this.routines.getOffset(this.element); - if (!this.settings.windowViewport) { - this.offset -= this.routines.getOffset(this.hostElement); - } + this.offset = this.routines.getOffset(); } findItemElementById(id: string): HTMLElement | null { - return this.routines.findItemElement(this.element, id); + return this.routines.findItemElement(id); } getEdgeVisibleItem(items: Item[], direction: Direction): { item?: Item, index: number, diff: number } { diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 37b83c5e..b93c5ef4 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -44,6 +44,7 @@ import { IAdapter, } from './adapter'; import { Settings, DevSettings } from './settings'; +import { IRoutines } from './routines'; import { ScrollEventData, ScrollState, State } from './state'; import { ProcessName, @@ -105,6 +106,7 @@ export { AdapterFixOptions, Settings, DevSettings, + IRoutines, ScrollEventData, ScrollState, State, diff --git a/src/scroller.ts b/src/scroller.ts index 029da3e7..96c8f049 100644 --- a/src/scroller.ts +++ b/src/scroller.ts @@ -36,16 +36,16 @@ export class Scroller { } const packageInfo = scroller ? scroller.state.packageInfo : ({ consumer, core } as IPackages); - element = scroller ? scroller.viewport.element : (element as HTMLElement); + element = scroller ? scroller.routines.element : (element as HTMLElement); workflow = scroller ? scroller.workflow : (workflow as ScrollerWorkflow); this.workflow = workflow; this.settings = new Settings(datasource.settings, datasource.devSettings, ++instanceCount); this.logger = new Logger(this as Scroller, packageInfo, datasource.adapter); - this.routines = new Routines(this.settings); + this.routines = new Routines(element, this.settings); this.state = new State(packageInfo, this.settings, scroller ? scroller.state : void 0); this.buffer = new Buffer(this.settings, workflow.onDataChanged, this.logger); - this.viewport = new Viewport(element, this.settings, this.routines, this.state, this.logger); + this.viewport = new Viewport(this.settings, this.routines, this.state, this.logger); this.logger.object('vscroll settings object', this.settings, true); this.initDatasource(datasource, scroller); diff --git a/src/workflow.ts b/src/workflow.ts index dedbe0cc..09e42981 100644 --- a/src/workflow.ts +++ b/src/workflow.ts @@ -69,14 +69,14 @@ export class Workflow { }); // set up scroll event listener - const { viewport: { scrollEventReceiver }, routines } = this.scroller; + const { routines } = this.scroller; const onScrollHandler: EventListener = event => this.callWorkflow({ process: CommonProcess.scroll, status: Status.start, payload: { event } }); - this.offScroll = routines.onScroll(scrollEventReceiver, onScrollHandler); + this.offScroll = routines.onScroll(onScrollHandler); } changeItems(items: Item[]): void { From b575392cab977ed729cf500ec0acb4505ff5f10a Mon Sep 17 00:00:00 2001 From: Denis Hilt Date: Mon, 22 Nov 2021 05:02:00 +0300 Subject: [PATCH 2/3] issue-29 IRoutines --- src/classes/domRoutines.ts | 9 +- src/interfaces/routines.ts | 185 +++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 src/interfaces/routines.ts diff --git a/src/classes/domRoutines.ts b/src/classes/domRoutines.ts index a0843afa..b20af69c 100644 --- a/src/classes/domRoutines.ts +++ b/src/classes/domRoutines.ts @@ -1,13 +1,10 @@ import { Settings } from './settings'; import { Direction } from '../inputs/index'; +import { IRoutines } from '../interfaces/index'; -export class Routines { +export class Routines implements IRoutines { - readonly settings: { - viewport: HTMLElement | null; - horizontal: boolean; - window: boolean; - }; + readonly settings: IRoutines['settings']; readonly element: HTMLElement; readonly viewport: HTMLElement; diff --git a/src/interfaces/routines.ts b/src/interfaces/routines.ts new file mode 100644 index 00000000..86c9ecbb --- /dev/null +++ b/src/interfaces/routines.ts @@ -0,0 +1,185 @@ +import { Settings } from '../classes/settings'; +import { Direction } from '../inputs/index'; + +interface IRoutinesSettings { + /** The scroller's viewport element defined by the app settings. + * The value is equal to settings.viewport and thus can be null. + */ + viewport: HTMLElement | null; + + /** Determines wether the scroller is horizontal-oriented or not. + * The value is equal to settings.horizontal. + */ + horizontal: boolean; + + /** Determines wether the entire window is the scroller's viewport or not. + * The value is equal to settings.window. + */ + window: boolean; +} + +export interface IRoutines { + + /** Internal prop that is available after instantiation. + * Reduced version of the App settings object. + */ + readonly settings: IRoutinesSettings; + + /** Internal prop that is available after instantiation. + * The scroller's element that comes from the end App. + */ + readonly element: HTMLElement; + + /** Internal prop that is available after instantiation. + * The scroller's viewport element. + * The "getViewportElement" method is responsible for the value. + */ + readonly viewport: HTMLElement; + + /** Checks HTML element. Should throw error if it's not valid. + * @param {HTMLElement} element HTML element to check. + */ + checkElement: (element: HTMLElement) => void; + + /** Gets the viewport element based on the internal props: + * "settings.viewport", "settings.window" and "element". + * This method is being called during Routines instantiation + * to determine the "viewport" prop: + * + * this.viewport = this.getViewportElement(); + * @returns {HTMLElement} HTML element. + */ + getViewportElement: () => HTMLElement; + + /** This method is being called in the end of Routines instantiation. + * @param {Settings} settings Unreduced Scroller's settings object. + */ + onInit: (settings: Settings) => void; + + /** Finds element by CSS selector. + * @param {HTMLElement} element Top of the elements hierarchy to search. + * @param {string} selector CSS selector. + * @returns {HTMLElement | null} The first HTML element that matches the specified selector, or null. + */ + findElementBySelector: (element: HTMLElement, selector: string) => HTMLElement | null; + + /** Finds padding element. + * @param {'backward' | 'forward'} direction Search direction: backward or forward. + * @returns {HTMLElement | null} HTML padding element, or null. + */ + findPaddingElement: (direction: Direction) => HTMLElement | null; + + /** Finds single item element by its id. + * @param {string} id Id of the element to search. + * @returns {HTMLElement | null} HTML item element, or null. + */ + findItemElement: (id: string) => HTMLElement | null; + + /** Gets scroll position of the viewport. Internal settings should be taken into account. + * @returns {number} Scroll position value. + */ + getScrollPosition: () => number; + + /** Sets scroll position of the viewport. Internal settings should be taken into account. + * @param {number} value Scroll position value. + */ + setScrollPosition: (value: number) => void; + + /** Gets the size of the element and its position relative to the viewport. + * @param {HTMLElement} element + * @returns {DOMRect} DOMRect object. + */ + getElementParams: (element: HTMLElement) => DOMRect; + + /** Gets params of the host element in case the "window" setting is set to true. + * @returns {DOMRect} DOMRect object. + */ + getWindowParams: () => DOMRect; + + /** Gets size of the element. Internal props should be taken into account. + * For example, if horizontal = false, then the element's height is needed. + * If horizontal = true, then the element's width is needed. + * @param {HTMLElement} element + * @returns {DOMRect} DOMRect object. + */ + getSize: (element: HTMLElement) => number; + + /** Gets size of the scroller element. + * @returns {DOMRect} DOMRect object. + */ + getScrollerSize: () => number; + + /** Gets size of the viewport. + * @returns {DOMRect} DOMRect object. + */ + getViewportSize: () => number; + + /** Gets size of the element. Internal settings ("horizontal") should be taken into account. + * This method should work in the same way as "setSizeStyle" does. + * @param {HTMLElement} element + * @returns {number} Numeric value. + */ + getSizeStyle: (element: HTMLElement) => number; + + /** Sets size of the element. Internal settings ("horizontal") should be taken into account. + * This method should work in the same way as "getSizeStyle" does. + * @param {HTMLElement} element + * @param {number} value Numeric value to be new element's size. + */ + setSizeStyle: (element: HTMLElement, value: number) => void; + + /** Gets the edge coordinate of the element. Internal settings ("horizontal") should be taken into account. + * For example, if horizontal = false and direction = "backward" then the element's top coordinate is needed. + * If horizontal = true and direction = "forward" then the element's right coordinate is needed. + * @param {HTMLElement} element + * @param {'backward' | 'forward'} direction + * @returns {number} Numeric value. + */ + getEdge: (element: HTMLElement, direction: Direction) => number; + + /** Gets the edge coordinate of the viewport. Internal settings should be taken into account. + * @param {'backward' | 'forward'} direction + * @returns {number} Numeric value. + */ + getViewportEdge: (direction: Direction) => number; + + /** Makes the element visible in the same way as the external Workflow.run method makes it invisible. + * @param {HTMLElement} element + */ + makeElementVisible: (element: HTMLElement) => void; + + /** Hides the element. This method is being called before remove and has no connection with makeElementVisible. + * @param {HTMLElement} element + */ + hideElement: (element: HTMLElement) => void; + + /** Gets scroller's offset. + * @returns {number} Numeric value. + */ + getOffset: () => number; + + /** Scrolls into the element's view. + * @param {HTMLElement} element + * @param {boolean | ScrollIntoViewOptions} argument + */ + scrollTo: (element: HTMLElement, argument?: boolean | ScrollIntoViewOptions) => void; + + /** Wraps rendering. Runs render process and calls the argument function when it is done. + * @param {function} cb + * @returns {function} Callback to dismiss render and prevent the argument function to be invoked. + */ + render: (cb: () => void) => () => void; + + /** Wraps animation. Runs animations process and calls the argument function when it is done. + * @param {function} cb + * @returns {function} Callback to dismiss animation and prevent the argument function to be invoked. + */ + animate: (cb: () => void) => () => void; + + /** Provides scroll event listening. Invokes the function argument each time the scroll event fires. + * @param {EventListener} handler + * @returns {function} Callback to dismiss scroll event listener. + */ + onScroll: (handler: EventListener) => () => void; + +} \ No newline at end of file From c993b6b04eaa76acdbf99c35fb70cf4090f2c480 Mon Sep 17 00:00:00 2001 From: Denis Hilt Date: Sat, 22 Jan 2022 03:39:20 +0300 Subject: [PATCH 3/3] v1.4.4 --- LICENSE | 2 +- README.md | 33 +++++++++++++++++---------------- package-lock.json | 2 +- package.json | 2 +- src/version.ts | 2 +- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/LICENSE b/LICENSE index d5b7f68a..c7a2f159 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Denis Hilt (https://github.com/dhilt) +Copyright (c) 2022 Denis Hilt (https://github.com/dhilt) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index fb8917a3..83e1a7da 100644 --- a/README.md +++ b/README.md @@ -140,17 +140,18 @@ This obliges the Datasource.get method to deal with _Data_ items and also provid A callback that is called every time the Workflow decides that the UI needs to be changed. Its argument is a list of items to be present in the UI. This is a consumer responsibility to detect changes and display them in the UI. ```js -run: items => { - // assume currentItems contains a list of items currently presented in the UI - if (!items.length && !currentItems.length) { +run: (newItems) => { + // assume oldItems contains a list of items that are currently present in the UI + if (!newItems.length && !oldItems.length) { return; } - displayNewItemsInsteadCurrentOnes(items, currentItems); - currentItems = items; -} + // make newItems to be present in the UI instead of oldItems + processItems(newItems, oldItems); + oldItems = newItems; +}; ``` -Each item is an instance of the [Item class](https://github.com/dhilt/vscroll/blob/v1.0.0/src/classes/item.ts) implementing the [Item interface](https://github.com/dhilt/vscroll/blob/v1.0.0/src/interfaces/item.ts), whose props can be used for proper implementation of the `run` callback: +Each item (in both `newItems` and `oldItems` lists) is an instance of the [Item class](https://github.com/dhilt/vscroll/blob/v1.0.0/src/classes/item.ts) implementing the [Item interface](https://github.com/dhilt/vscroll/blob/v1.0.0/src/interfaces/item.ts), whose props can be used for proper implementation of the `run` callback: |Name|Type|Description| |:--|:--|:----| @@ -162,19 +163,19 @@ Each item is an instance of the [Item class](https://github.com/dhilt/vscroll/bl `Run` callback is the most complex and environment-specific part of the `vscroll` API, which is fully depends on the environment for which the consumer is being created. Framework specific consumer should rely on internal mechanism of the framework to provide runtime DOM modifications. -There are some requirements on how the `items` should be processed by `run(items)` call: - - after the `run(items)` callback is completed, there must be `items.length` elements in the DOM between backward and forward padding elements; - - old items that are not in the list should be removed from DOM; use `currentItems[].element` reference for this purpose; - - old items that are in the list should not be removed and recreated, as it may lead to an unwanted shift of the scroll position; just don't touch them; - - new items elements should be rendered in accordance with `items[].$index` comparable to `$index` of elements that remain: `$index` must increase continuously and the directions of increase must persist across the `run` calls; Scroller maintains `$index` internally, so you only need to properly inject the `items[].element` into the DOM; - - new elements should be rendered but not visible, and this should be achieved by "fixed" positioning and "left"/"top" coordinates placing the item element out of view; the Workflow will take care of visibility after calculations; an additional attribute `items[].invisible` can be used to determine if a given element should be hidden; - - new items elements should have "data-sid" attribute, which value should reflect `items[].$index`; +There are some requirements on how the items should be processed by `run` call: + - after the `run` callback is completed, there must be `newItems.length` elements in the DOM between backward and forward padding elements; +- old items that are not in the new item list should be removed from DOM; use `oldItems[].element` references for this purpose; + - old items that are in the list should not be removed and recreated, as it may lead to an unwanted shift of the scroll position; just don't touch them; + - new items elements should be rendered in accordance with `newItems[].$index` comparable to `$index` of elements that remain: `$index` must increase continuously and the directions of increase must persist across the `run` calls; Scroller maintains `$index` internally, so you only need to properly inject a set of `newItems[].element` into the DOM; + - new elements should be rendered but not visible, and this should be achieved by "fixed" positioning and "left"/"top" coordinates placing the item element out of view; the Workflow will take care of visibility after calculations; an additional attribute `newItems[].invisible` can be used to determine if a given element should be hidden; + - new items elements should have "data-sid" attribute, which value should reflect `newItems[].$index`; ## Live This repository has a minimal demonstration of the App-consumer implementation considering all of the requirements listed above: https://dhilt.github.io/vscroll/. This is all-in-one HTML demo with `vscroll` taken from CDN. The source code of the demo is [here](https://github.com/dhilt/vscroll/blob/main/demo/index.html). The approach is rough and non-optimized, if you are seeking for more general solution for native JavaScript applications, please take a look at [vscroll-native](https://github.com/dhilt/vscroll-native) project. It is relatively new and has no good documentation, but its [source code](https://github.com/dhilt/vscroll-native/tree/main/src) and [demo](https://github.com/dhilt/vscroll-native/tree/main/demo) may shed light on `vscroll` usage in no-framework environment. -Another example is [ngx-ui-scroll](https://github.com/dhilt/ngx-ui-scroll). Before 2021 `vscroll` was part of `ngx-ui-scroll`, and its [demo page](https://dhilt.github.io/ngx-ui-scroll/#/) contains well-documented samples that can be used to get an idea on the API and functionality offered by `vscroll`. The code of the [UiScrollComponent](https://github.com/dhilt/ngx-ui-scroll/blob/v2.0.0-rc.1/src/ui-scroll.component.ts) clearly demonstrates the `Workflow` instantiation in the context of Angular. Also, since ngx-ui-scroll is the intermediate layer between `vscroll` and the end Application, the Datasource is being provided from the outside. Method `makeDatasource` is used to provide `Datasource` class to the end Application. +Another example is [ngx-ui-scroll](https://github.com/dhilt/ngx-ui-scroll). Before 2021 `vscroll` was part of `ngx-ui-scroll`, and its [demo page](https://dhilt.github.io/ngx-ui-scroll/#/) contains well-documented samples that can be used to get an idea on the API and functionality offered by `vscroll`. The code of the [UiScrollComponent](https://github.com/dhilt/ngx-ui-scroll/blob/v2.2.0/src/ui-scroll.component.ts) clearly demonstrates the `Workflow` instantiation in the context of Angular. Also, since ngx-ui-scroll is the intermediate layer between `vscroll` and the end Application, the Datasource is being provided from the outside. Method `makeDatasource` is used to provide `Datasource` class to the end Application. ## Adapter API @@ -223,4 +224,4 @@ VScroll will receive its own Adapter API documentation later, but for now please __________ -2021 © [Denis Hilt](https://github.com/dhilt) +2022 © [Denis Hilt](https://github.com/dhilt) diff --git a/package-lock.json b/package-lock.json index 5afc9abf..4f861d5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscroll", - "version": "1.4.3", + "version": "1.4.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index de93a397..4e914d1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vscroll", - "version": "1.4.3", + "version": "1.4.4", "description": "Virtual scroll engine", "main": "dist/bundles/vscroll.umd.js", "module": "dist/bundles/vscroll.esm5.js", diff --git a/src/version.ts b/src/version.ts index 5ae27bbe..afca0e4d 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,4 +1,4 @@ export default { name: 'vscroll', - version: '1.4.3' + version: '1.4.4', };