vscroll
Version:
Virtual scroll engine
487 lines • 17.9 kB
JavaScript
import { Reactive } from './reactive';
import { AdapterPropName, AdapterPropType, EMPTY_ITEM, getDefaultAdapterProps, methodPausedResult, methodPreResult, reactiveConfigStorage } from './adapter/props';
import { wantedUtils } from './adapter/wanted';
import { Direction } from '../inputs/index';
import { AdapterProcess, ProcessStatus } from '../processes/index';
const ADAPTER_PROPS_STUB = getDefaultAdapterProps();
const ALLOWED_METHODS_WHEN_PAUSED = ADAPTER_PROPS_STUB.filter(v => !!v.allowedWhenPaused).map(v => v.name);
const _has = (obj, prop) => !!obj && typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, prop);
const convertAppendArgs = (prepend, options, eof) => {
let result = options;
if (!_has(options, 'items')) {
const items = !Array.isArray(options) ? [options] : options;
result = prepend ? { items, bof: eof } : { items, eof: eof };
}
return result;
};
const convertRemoveArgs = (options) => {
if (!(_has(options, 'predicate') || _has(options, 'indexes'))) {
const predicate = options;
options = { predicate };
}
return options;
};
export class Adapter {
get workflow() {
return this.getWorkflow();
}
get reloadCount() {
return this.reloadCounter;
}
get reloadId() {
return this.id + '.' + this.reloadCounter;
}
getPromisifiedMethod(method, args) {
return new Promise(resolve => {
if (this.relax$) {
this.relax$.once(value => resolve(value));
}
method.apply(this, args);
});
}
getWorkflowRunnerMethod(method, name) {
return (...args) => {
var _a, _b, _c, _d;
if (!this.relax$) {
(_b = (_a = this.logger) === null || _a === void 0 ? void 0 : _a.log) === null || _b === void 0 ? void 0 : _b.call(_a, () => 'scroller is not initialized: ' + name + ' method is ignored');
return Promise.resolve(methodPreResult);
}
if (this.paused && !ALLOWED_METHODS_WHEN_PAUSED.includes(name)) {
(_d = (_c = this.logger) === null || _c === void 0 ? void 0 : _c.log) === null || _d === void 0 ? void 0 : _d.call(_c, () => 'scroller is paused: ' + name + ' method is ignored');
return Promise.resolve(methodPausedResult);
}
return this.getPromisifiedMethod(method, args);
};
}
constructor(context, getWorkflow, logger) {
this.source = {}; // for Reactive props
this.box = {}; // for Scalars over Reactive props
this.demand = {}; // for Scalars on demand
this.setFirstOrLastVisible = (_) => { };
this.getWorkflow = getWorkflow;
this.logger = logger;
this.relax$ = null;
this.relaxRun = null;
this.reloadCounter = 0;
const contextId = (context === null || context === void 0 ? void 0 : context.id) || -1;
// public context (if exists) should provide access to Reactive props config by id
const reactivePropsStore = (context && reactiveConfigStorage.get(context.id)) || {};
// the Adapter initialization should not trigger "wanted" props setting;
// after the initialization is completed, "wanted" functionality must be unblocked
wantedUtils.setBlock(true, contextId);
// make array of the original values from public context if present
const adapterProps = context
? ADAPTER_PROPS_STUB.map(prop => {
let value = context[prop.name];
// if context is augmented, we need to replace external reactive props with inner ones
if (context.augmented) {
const reactiveProp = reactivePropsStore[prop.name];
if (reactiveProp) {
value = reactiveProp.default; // boolean doesn't matter here
}
}
return Object.assign(Object.assign({}, prop), { value });
})
: getDefaultAdapterProps();
// restore default reactive props if they were configured
Object.entries(reactivePropsStore).forEach(([key, value]) => {
const prop = adapterProps.find(({ name }) => name === key);
if (prop && value) {
prop.value = value.default;
}
});
// Scalar permanent props
adapterProps
.filter(({ type, permanent }) => type === AdapterPropType.Scalar && permanent)
.forEach(({ name, value }) => Object.defineProperty(this, name, {
configurable: true,
get: () => value
}));
// Reactive props: store original values in "source" container, to avoid extra .get() calls on scalar twins set
adapterProps
.filter(prop => prop.type === AdapterPropType.Reactive)
.forEach(({ name, value }) => {
this.source[name] = value;
Object.defineProperty(this, name, {
configurable: true,
get: () => this.source[name]
});
});
// for "wanted" props that can be explicitly requested for the first time after the Adapter initialization,
// an implicit calculation of the initial value is required;
// so this method should be called when accessing the "wanted" props through one of the following getters
const processWanted = (prop) => {
if (wantedUtils.setBox(prop, contextId)) {
const firstPropList = [AdapterPropName.firstVisible, AdapterPropName.firstVisible$];
const lastPropList = [AdapterPropName.lastVisible, AdapterPropName.lastVisible$];
if (firstPropList.some(n => n === prop.name)) {
this.setFirstOrLastVisible({ first: true });
}
else if (lastPropList.some(n => n === prop.name)) {
this.setFirstOrLastVisible({ last: true });
}
}
};
// Scalar props that have Reactive twins
// 1) reactive props (from "source") should be triggered on set
// 2) scalars should use "box" container on get
// 3) "wanted" scalars should also run wanted-related logic on get
adapterProps
.filter(prop => prop.type === AdapterPropType.Scalar && !!prop.reactive)
.forEach((prop) => {
const { name, value, reactive } = prop;
this.box[name] = value;
Object.defineProperty(this, name, {
configurable: true,
set: (newValue) => {
if (newValue !== this.box[name]) {
this.box[name] = newValue;
this.source[reactive].set(newValue);
// need to emit new value through the configured reactive prop if present
const reactiveProp = reactivePropsStore[reactive];
if (reactiveProp) {
reactiveProp.emit(reactiveProp.source, newValue);
}
}
},
get: () => {
processWanted(prop);
return this.box[name];
}
});
});
// Scalar props on-demand
// these scalars should use "demand" container
// setting defaults should be overridden on init()
adapterProps
.filter(prop => prop.type === AdapterPropType.Scalar && prop.onDemand)
.forEach(({ name, value }) => {
this.demand[name] = value;
Object.defineProperty(this, name, {
configurable: true,
get: () => this.demand[name]
});
});
if (!context) {
return;
}
// Adapter public context augmentation
adapterProps.forEach((prop) => {
const { name, type, permanent } = prop;
let value = this[name];
if (type === AdapterPropType.Function) {
value = value.bind(this);
}
else if (type === AdapterPropType.WorkflowRunner) {
value = this.getWorkflowRunnerMethod(value, name);
}
else if (type === AdapterPropType.Reactive && reactivePropsStore[name]) {
value = context[name];
}
else if (name === AdapterPropName.augmented) {
value = true;
}
const nonPermanentScalar = !permanent && type === AdapterPropType.Scalar;
Object.defineProperty(context, name, {
configurable: true,
get: () => {
processWanted(prop); // consider accessing "wanted" Reactive props
if (nonPermanentScalar) {
return this[name]; // non-permanent Scalars should be taken in runtime
}
return value; // other props (Reactive/Functions/WorkflowRunners) can be defined once
}
});
});
this.externalContext = context;
wantedUtils.setBlock(false, contextId);
}
initialize({ buffer, state, viewport, logger, adapterRun$, getWorkflow }) {
// buffer
Object.defineProperty(this.demand, AdapterPropName.itemsCount, {
get: () => buffer.getVisibleItemsCount()
});
Object.defineProperty(this.demand, AdapterPropName.bufferInfo, {
get: () => ({
firstIndex: buffer.firstIndex,
lastIndex: buffer.lastIndex,
minIndex: buffer.minIndex,
maxIndex: buffer.maxIndex,
absMinIndex: buffer.absMinIndex,
absMaxIndex: buffer.absMaxIndex,
defaultSize: buffer.defaultSize
})
});
this.bof = buffer.bof.get();
buffer.bof.on(bof => (this.bof = bof));
this.eof = buffer.eof.get();
buffer.eof.on(eof => (this.eof = eof));
// state
Object.defineProperty(this.demand, AdapterPropName.packageInfo, {
get: () => state.packageInfo
});
this.loopPending = state.cycle.innerLoop.busy.get();
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 }) => {
var _a, _b, _c;
if ((!first && !last) || ((_a = workflow === null || workflow === void 0 ? void 0 : workflow.call) === null || _a === void 0 ? void 0 : _a.interrupted)) {
return;
}
const token = first ? AdapterPropName.firstVisible : AdapterPropName.lastVisible;
if (!((_c = wantedUtils.getBox((_b = this.externalContext) === null || _b === void 0 ? void 0 : _b.id)) === null || _c === void 0 ? void 0 : _c[token])) {
return;
}
if (buffer.items.some(({ element }) => !element)) {
logger.log('skipping first/lastVisible set because not all buffered items are rendered at this moment');
return;
}
const direction = first ? Direction.backward : Direction.forward;
const { item } = viewport.getEdgeVisibleItem(buffer.items, direction);
if (!item || item.element !== this[token].element) {
this[token] = (item ? item.get() : EMPTY_ITEM);
}
};
// logger
this.logger = logger;
// self-pending subscription; set up only on the very first init
if (adapterRun$) {
if (!this.relax$) {
this.relax$ = new Reactive();
}
const relax$ = this.relax$;
adapterRun$.on(({ status, payload }) => {
let unSubRelax = () => { };
if (status === ProcessStatus.start) {
unSubRelax = this.isLoading$.on(value => {
if (!value) {
unSubRelax();
relax$.set({ success: true, immediate: false, details: null });
}
});
}
else if (status === ProcessStatus.done || status === ProcessStatus.error) {
unSubRelax();
relax$.set({
success: status !== ProcessStatus.error,
immediate: true,
details: status === ProcessStatus.error && payload ? String(payload.error) : null
});
}
});
}
// workflow getter
if (getWorkflow) {
this.getWorkflow = getWorkflow;
}
// init
this.init = true;
}
dispose() {
if (this.relax$) {
this.relax$.dispose();
}
if (this.externalContext) {
this.resetContext();
}
Object.getOwnPropertyNames(this).forEach(prop => {
delete this[prop];
});
this.disposed = true;
}
resetContext() {
var _a;
const reactiveStore = reactiveConfigStorage.get((_a = this.externalContext) === null || _a === void 0 ? void 0 : _a.id);
ADAPTER_PROPS_STUB.forEach(({ type, permanent, name, value }) => {
// assign initial values to non-reactive non-permanent props
if (type !== AdapterPropType.Reactive && !permanent) {
Object.defineProperty(this.externalContext, name, {
configurable: true,
get: () => value
});
}
// reset reactive props
if (type === AdapterPropType.Reactive && reactiveStore) {
const property = reactiveStore[name];
if (property) {
property.default.reset();
property.emit(property.source, property.default.get());
}
}
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reset(options) {
this.reloadCounter++;
this.logger.logAdapterMethod('reset', options, ` of ${this.reloadId}`);
this.workflow.call({
process: AdapterProcess.reset,
status: ProcessStatus.start,
payload: { options }
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reload(options) {
this.reloadCounter++;
this.logger.logAdapterMethod('reload', options, ` of ${this.reloadId}`);
this.workflow.call({
process: AdapterProcess.reload,
status: ProcessStatus.start,
payload: { options }
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
append(_options, eof) {
const options = convertAppendArgs(false, _options, eof); // support old signature
this.logger.logAdapterMethod('append', [options.items, options.eof]);
this.workflow.call({
process: AdapterProcess.append,
status: ProcessStatus.start,
payload: { options }
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
prepend(_options, bof) {
const options = convertAppendArgs(true, _options, bof); // support old signature
this.logger.logAdapterMethod('prepend', [options.items, options.bof]);
this.workflow.call({
process: AdapterProcess.prepend,
status: ProcessStatus.start,
payload: { options }
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
check() {
this.logger.logAdapterMethod('check');
this.workflow.call({
process: AdapterProcess.check,
status: ProcessStatus.start
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
remove(options) {
options = convertRemoveArgs(options); // support old signature
this.logger.logAdapterMethod('remove', options);
this.workflow.call({
process: AdapterProcess.remove,
status: ProcessStatus.start,
payload: { options }
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
clip(options) {
this.logger.logAdapterMethod('clip', options);
this.workflow.call({
process: AdapterProcess.clip,
status: ProcessStatus.start,
payload: { options }
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
insert(options) {
this.logger.logAdapterMethod('insert', options);
this.workflow.call({
process: AdapterProcess.insert,
status: ProcessStatus.start,
payload: { options }
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
replace(options) {
this.logger.logAdapterMethod('replace', options);
this.workflow.call({
process: AdapterProcess.replace,
status: ProcessStatus.start,
payload: { options }
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
update(options) {
this.logger.logAdapterMethod('update', options);
this.workflow.call({
process: AdapterProcess.update,
status: ProcessStatus.start,
payload: { options }
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pause() {
this.logger.logAdapterMethod('pause');
this.workflow.call({
process: AdapterProcess.pause,
status: ProcessStatus.start
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resume() {
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) {
this.logger.logAdapterMethod('fix', options);
this.workflow.call({
process: AdapterProcess.fix,
status: ProcessStatus.start,
payload: { options }
});
}
relaxUnchained(callback, reloadId) {
const runCallback = () => typeof callback === 'function' && reloadId === this.reloadId && callback();
if (!this.isLoading) {
runCallback();
}
return new Promise(resolve => {
if (!this.isLoading) {
resolve(true);
return;
}
this.isLoading$.once(() => {
runCallback();
resolve(false);
});
}).then(immediate => {
var _a, _b;
if (this.disposed) {
return {
immediate,
success: false,
details: 'Adapter was disposed'
};
}
const success = reloadId === this.reloadId;
(_b = (_a = this.logger) === null || _a === void 0 ? void 0 : _a.log) === null || _b === void 0 ? void 0 : _b.call(_a, () => !success ? `relax promise cancelled due to ${reloadId} != ${this.reloadId}` : void 0);
return {
immediate,
success,
details: !success ? 'Interrupted by reload or reset' : null
};
});
}
relax(callback) {
const reloadId = this.reloadId;
this.logger.logAdapterMethod('relax', callback, ` of ${reloadId}`);
if (!this.init) {
return Promise.resolve(methodPreResult);
}
return (this.relaxRun = this.relaxRun
? this.relaxRun.then(() => this.relaxUnchained(callback, reloadId))
: this.relaxUnchained(callback, reloadId).then(result => {
this.relaxRun = null;
return result;
}));
}
showLog() {
this.logger.logAdapterMethod('showLog');
this.logger.logForce();
}
}
//# sourceMappingURL=adapter.js.map