Skip to content

Commit

Permalink
Merge branch 'main' into e2e
Browse files Browse the repository at this point in the history
  • Loading branch information
dhilt committed Mar 17, 2024
2 parents 99fc3dc + a5035b4 commit add47f0
Show file tree
Hide file tree
Showing 18 changed files with 174 additions and 35 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Denis Hilt (https://github.com/dhilt)
Copyright (c) 2024 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
Expand Down
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ interface WorkflowParams<ItemData> {
}
```

This is a TypeScript definition, but speaking of JavaScript, an argument object must contain 4 fields described below.
This is a TypeScript definition, but speaking of JavaScript, an argument object must contain 4 mandatory and 1 optional fields described below.

### 1. Consumer

Expand Down Expand Up @@ -176,13 +176,14 @@ Each item (in both `newItems` and `oldItems` lists) is an instance of the [Item

`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` 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; this requirement can be changed by the `Routines` class setting, see below;
- new items elements should have "data-sid" attribute, which value should reflect `newItems[].$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 items list should be removed from DOM. Use `oldItems[].element` references for this purpose.
- Old items that are in the new items list should not be removed and recreated, as this may result in unwanted scroll position shifts. Just don't touch them.
- New items elements should be rendered in the correct order. Specifically, 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. The scroller maintains `$index` internally, so you only need to properly inject a set of `newItems[].element` into the DOM.
- New elements should be rendered without being visible, and this should be achieved by "fixed" positioning and "left"/"top" coordinates that take the item element out of view. The Workflow will take care of visibility after calculations. An additional `newItems[].invisible` attribute can be used to determine whether a given element should be hidden. This requirement can be changed by the `Routines` class setting (see below).
- New items elements should have a "data-sid" attribute whose value should reflect `newItems[].$index`.

### 5. Routines

Expand All @@ -194,7 +195,7 @@ import { Routines, Workflow } from 'vscroll';
class CustomRoutines extends Routines { ... }

new Workflow({
// consumer, element, datasource, run,
consumer, element, datasource, run, // required params
Routines: CustomRoutines
})
```
Expand All @@ -213,9 +214,9 @@ If we have a table layout case where we need to specify the offset of the table

```js
new Workflow({
// consumer, element, datasource, run,
consumer, element, datasource, run, // required params
Routines: class extends Routines {
getOffset(element) {
getOffset() {
return document.querySelector('#viewport thead')?.offsetHeight || 0;
}
}
Expand Down Expand Up @@ -292,4 +293,4 @@ VScroll will receive its own Adapter API documentation later, but for now please
__________
2023 &copy; [Denis Hilt](https://github.com/dhilt)
2024 &copy; [Denis Hilt](https://github.com/dhilt)
3 changes: 2 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ <h2 id="project_tagline">
</div>
</div>
<div class="column">
<p id="vscroll-core-version"> </p>
<p>
This is a minimal working demo of a virtual scroll list
rendering an unlimited data in runtime.
Expand All @@ -46,7 +47,7 @@ <h2 id="project_tagline">
</div>
<div id="footer_wrap" class="outer">
<footer class="inner">
<p class="copyright">2023 © <a href="https://github.com/dhilt">Denis Hilt</a></p>
<p class="copyright">2024 © <a href="https://github.com/dhilt">Denis Hilt</a></p>
</footer>
</div>
<script src="./index.js"></script>
Expand Down
3 changes: 3 additions & 0 deletions demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,6 @@ const createItemElement = item => {

// run the VScroll Workflow
new VScroll.Workflow(workflowParams);

const versionElt = document.getElementById(`vscroll-core-version`);
versionElt.innerHTML = `Package version: ${VScroll.packageInfo.name} v${VScroll.packageInfo.version}.`;
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vscroll",
"version": "1.6.0-beta.3",
"version": "1.6.1",
"description": "Virtual scroll engine",
"main": "dist/bundles/vscroll.umd.js",
"module": "dist/bundles/vscroll.esm5.js",
Expand Down
70 changes: 56 additions & 14 deletions src/classes/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { Logger } from './logger';
import { Buffer } from './buffer';
import { Reactive } from './reactive';
import {
AdapterPropName, AdapterPropType, EMPTY_ITEM, getDefaultAdapterProps, methodPreResult, reactiveConfigStorage
AdapterPropName,
AdapterPropType,
EMPTY_ITEM,
getDefaultAdapterProps,
methodPausedResult,
methodPreResult,
reactiveConfigStorage
} from './adapter/props';
import { wantedUtils } from './adapter/wanted';
import { Viewport } from './viewport';
Expand Down Expand Up @@ -42,9 +48,10 @@ type InitializationParams<Item> = {
}

const ADAPTER_PROPS_STUB = getDefaultAdapterProps();
const ALLOWED_METHODS_WHEN_PAUSED = ADAPTER_PROPS_STUB.filter(v => !!v.allowedWhenPaused).map(v => v.name);

const _has = (obj: unknown, prop: string): boolean =>
typeof obj === 'object' && obj !== null && Object.prototype.hasOwnProperty.call(obj, prop);
!!obj && typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, prop);

const convertAppendArgs = <Item>(prepend: boolean, options: unknown, eof?: boolean) => {
let result = options as AdapterAppendOptions<Item> & AdapterPrependOptions<Item>;
Expand Down Expand Up @@ -107,20 +114,34 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
bof$: Reactive<boolean>;
eof: boolean;
eof$: Reactive<boolean>;
paused: boolean;
paused$: Reactive<boolean>;

private relax$: Reactive<AdapterMethodResult> | null;
private relaxRun: Promise<AdapterMethodResult> | null;

private getPromisifiedMethod(method: MethodResolver, defaultMethod: MethodResolver) {
return (...args: unknown[]): Promise<AdapterMethodResult> =>
this.relax$
? new Promise(resolve => {
if (this.relax$) {
this.relax$.once(value => resolve(value));
}
method.apply(this, args);
})
: defaultMethod.apply(this, args);
private getPromisifiedMethod(method: MethodResolver, args: unknown[]) {
return new Promise<AdapterMethodResult>(resolve => {
if (this.relax$) {
this.relax$.once(value => resolve(value));
}
method.apply(this, args);
});
}

private getWorkflowRunnerMethod(method: MethodResolver, name: AdapterPropName) {
return (...args: unknown[]): Promise<AdapterMethodResult> => {
if (!this.relax$) {
this.logger?.log?.(() => 'scroller is not initialized: ' + name + ' method is ignored');
return Promise.resolve(methodPreResult);
}
if (this.paused && !ALLOWED_METHODS_WHEN_PAUSED.includes(name)) {
this.logger?.log?.(() => 'scroller is paused: ' + name + ' method is ignored');
return Promise.resolve(methodPausedResult);

}
return this.getPromisifiedMethod(method, args);
};
}

constructor(context: IAdapter<Item> | null, getWorkflow: WorkflowGetter<Item>, logger: Logger) {
Expand Down Expand Up @@ -244,12 +265,12 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
// Adapter public context augmentation
adapterProps
.forEach((prop: IAdapterProp) => {
const { name, type, value: defaultValue, permanent } = prop;
const { name, type, permanent } = prop;
let value = (this as IAdapter)[name];
if (type === AdapterPropType.Function) {
value = (value as () => void).bind(this);
} else if (type === AdapterPropType.WorkflowRunner) {
value = this.getPromisifiedMethod(value as MethodResolver, defaultValue as MethodResolver);
value = this.getWorkflowRunnerMethod(value as MethodResolver, name);
} else if (type === AdapterPropType.Reactive && reactivePropsStore[name]) {
value = (context as IAdapter)[name];
} else if (name === AdapterPropName.augmented) {
Expand Down Expand Up @@ -303,6 +324,8 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
state.cycle.innerLoop.busy.on(busy => this.loopPending = busy);
this.isLoading = state.cycle.busy.get();
state.cycle.busy.on(busy => this.isLoading = busy);
this.paused = state.paused.get();
state.paused.on(paused => this.paused = paused);

//viewport
this.setFirstOrLastVisible = ({ first, last, workflow }) => {
Expand Down Expand Up @@ -501,6 +524,25 @@ export class Adapter<Item = unknown> implements IAdapter<Item> {
});
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
pause(): any {
this.logger.logAdapterMethod('pause');
this.workflow.call({
process: AdapterProcess.pause,
status: ProcessStatus.start
});
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
resume(): any {
this.logger.logAdapterMethod('resume');
this.workflow.call({
process: AdapterProcess.pause,
status: ProcessStatus.start,
payload: { options: { resume: true } }
});
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
fix(options: AdapterFixOptions<Item>): any {
this.logger.logAdapterMethod('fix', options);
Expand Down
35 changes: 34 additions & 1 deletion src/classes/adapter/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export enum AdapterPropName {
bof$ = 'bof$',
eof = 'eof',
eof$ = 'eof$',
paused = 'paused',
paused$ = 'paused$',
reset = 'reset',
reload = 'reload',
append = 'append',
Expand All @@ -35,6 +37,8 @@ export enum AdapterPropName {
insert = 'insert',
replace = 'replace',
update = 'update',
pause = 'pause',
resume = 'resume',
fix = 'fix',
relax = 'relax',
showLog = 'showLog',
Expand All @@ -58,6 +62,12 @@ export const methodPreResult: AdapterMethodResult = {
details: 'Adapter is not initialized'
};

export const methodPausedResult: AdapterMethodResult = {
immediate: true,
success: true,
details: 'Scroller is paused'
};

const noopWF = () => Promise.resolve(methodPreResult);

const emptyPackageInfo: IPackages = {
Expand Down Expand Up @@ -173,10 +183,17 @@ export const getDefaultAdapterProps = (): IAdapterProp[] => [
value: false,
reactive: Name.eof$
},
{
type: Type.Scalar,
name: Name.paused,
value: false,
reactive: Name.paused$
},
{
type: Type.WorkflowRunner,
name: Name.reset,
value: noopWF
value: noopWF,
allowedWhenPaused: true
},
{
type: Type.WorkflowRunner,
Expand Down Expand Up @@ -223,6 +240,17 @@ export const getDefaultAdapterProps = (): IAdapterProp[] => [
name: Name.update,
value: noopWF
},
{
type: Type.WorkflowRunner,
name: Name.pause,
value: noopWF
},
{
type: Type.WorkflowRunner,
name: Name.resume,
value: noopWF,
allowedWhenPaused: true
},
{
type: Type.WorkflowRunner,
name: Name.fix,
Expand Down Expand Up @@ -274,6 +302,11 @@ export const getDefaultAdapterProps = (): IAdapterProp[] => [
type: Type.Reactive,
name: Name.eof$,
value: new Reactive<boolean>()
},
{
type: Type.Reactive,
name: Name.paused$,
value: new Reactive<boolean>()
}
];

Expand Down
4 changes: 4 additions & 0 deletions src/classes/state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Settings } from './settings';
import { Reactive } from './reactive';
import { WorkflowCycleModel } from './state/cycle';
import { FetchModel } from './state/fetch';
import { ClipModel } from './state/clip';
Expand All @@ -11,6 +12,7 @@ export class State implements IState {
readonly packageInfo: IPackages;
private settings: Settings;
initTime: number;
paused: Reactive<boolean>;

cycle: WorkflowCycleModel;
fetch: FetchModel;
Expand All @@ -26,6 +28,7 @@ export class State implements IState {
this.packageInfo = packageInfo;
this.settings = settings;
this.initTime = Number(new Date());
this.paused = new Reactive(false);

this.cycle = new WorkflowCycleModel(this.settings.instanceIndex, state ? state.cycle : void 0);
this.fetch = new FetchModel(settings.directionPriority);
Expand Down Expand Up @@ -80,6 +83,7 @@ export class State implements IState {
dispose(): void {
this.scroll.stop();
this.cycle.dispose();
this.paused.dispose();
this.endInnerLoop();
}

Expand Down
2 changes: 2 additions & 0 deletions src/inputs/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export const AdapterMethods: AdapterProcessMap<{ [key: string]: string }> = {
[Process.insert]: AdapterInsertParams,
[Process.replace]: AdapterReplaceParams,
[Process.update]: AdapterUpdateParams,
[Process.pause]: AdapterNoParams,
[Process.fix]: AdapterFixParams,
};

Expand All @@ -251,5 +252,6 @@ export const ADAPTER_METHODS: AdapterProcessMap<ICommonProps<PropertyKey>> = {
[Process.insert]: INSERT_METHOD_PARAMS,
[Process.replace]: REPLACE_METHOD_PARAMS,
[Process.update]: UPDATE_METHOD_PARAMS,
[Process.pause]: NO_METHOD_PARAMS,
[Process.fix]: FIX_METHOD_PARAMS,
};
5 changes: 5 additions & 0 deletions src/interfaces/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface IAdapterProp {
wanted?: boolean;
onDemand?: boolean;
permanent?: boolean;
allowedWhenPaused?: boolean;
}

export interface ItemAdapter<Data = unknown> {
Expand Down Expand Up @@ -157,6 +158,8 @@ export interface IAdapter<Data = unknown> {
readonly bof$: Reactive<boolean>;
readonly eof: boolean;
readonly eof$: Reactive<boolean>;
readonly paused: boolean;
readonly paused$: Reactive<boolean>;
reset(datasource?: IDatasourceOptional): MethodResult;
reload(reloadIndex?: number | string): MethodResult;
append(options: AdapterAppendOptions<Data>): MethodResult;
Expand All @@ -170,6 +173,8 @@ export interface IAdapter<Data = unknown> {
insert(options: AdapterInsertOptions<Data>): MethodResult;
replace(options: AdapterReplaceOptions<Data>): MethodResult;
update(options: AdapterUpdateOptions<Data>): MethodResult;
pause(): MethodResult;
resume(): MethodResult;
fix(options: AdapterFixOptions<Data>): MethodResult; // experimental
relax(callback?: () => void): MethodResult;
showLog(): void;
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Direction } from '../inputs/index';
import { Reactive } from '../classes/reactive';
import { WorkflowCycleModel } from '../classes/state/cycle';
import { FetchModel } from '../classes/state/fetch';
import { ClipModel } from '../classes/state/clip';
Expand All @@ -15,6 +16,7 @@ export interface ScrollEventData {
export interface State {
packageInfo: IPackages;
initTime: number;
paused: Reactive<boolean>;
cycle: WorkflowCycleModel;
fetch: FetchModel;
clip: ClipModel;
Expand Down
Loading

0 comments on commit add47f0

Please sign in to comment.