import * as sort from "../internal/sort"; import type { DataSourceAPI, FieldAssessor, FieldData, FieldDef, MaybePromise, MaybePromiseOrCall, MaybePromiseOrCallOrUndef, MaybePromiseOrUndef, } from "../ts-types"; import { applyChainSafe, emptyFn, getOrApply, isPromise, obj, } from "../internal/utils"; import { EventTarget } from "../core/EventTarget"; import type { PromiseCacheValue } from "./internal/types"; /** @private */ function isFieldAssessor(field: FieldDef): field is FieldAssessor { if (obj.isObject(field)) { if ((field as FieldAssessor).get && (field as FieldAssessor).set) { return true; } } return false; } /** @private */ const EVENT_TYPE = { UPDATE_LENGTH: "update_length", UPDATED_LENGTH: "updated_length", UPDATED_ORDER: "updated_order", } as const; /** @private */ type PromiseBack = (value: PromiseCacheValue) => void; /** @private */ type CompareResult = -1 | 0 | 1; /** @private */ function ascOrderFn(v1: T, v2: T): CompareResult { if (v1 === v2) { return 0; } if (v1 == null) { return v2 == null ? // If both are nullish, consider a match. 0 : // Nulls first -1; } if (v2 == null) { // Nulls first return 1; } return v1 > v2 ? 1 : -1; } function descOrderFn(v1: T, v2: T): CompareResult { return (ascOrderFn(v1, v2) * -1) as CompareResult; } /** @private */ function getValue( value: MaybePromiseOrCallOrUndef, setPromiseBack: PromiseBack ): MaybePromiseOrUndef { const maybePromiseValue = getOrApply(value); if (isPromise(maybePromiseValue)) { const promiseValue = maybePromiseValue.then((r: V | undefined) => { setPromiseBack(r); return r; }); //一時的にキャッシュ setPromiseBack(promiseValue); return promiseValue; } else { return maybePromiseValue; } } /** @private */ function getField>( record: MaybePromiseOrUndef, field: F, // eslint-disable-next-line @typescript-eslint/no-explicit-any setPromiseBack: PromiseBack ): FieldData { if (record == null) { return undefined; } if (isPromise(record)) { return record.then((r: T | undefined) => getField(r, field, setPromiseBack) ); } const fieldGet = isFieldAssessor(field) ? field.get : field; if ((fieldGet as never) in (record as never)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const fieldResult = (record as any)[fieldGet]; // eslint-disable-next-line @typescript-eslint/no-explicit-any return getValue(fieldResult, setPromiseBack); } if (typeof fieldGet === "function") { // eslint-disable-next-line @typescript-eslint/no-explicit-any const fieldResult = (fieldGet as any)(record); return getValue(fieldResult, setPromiseBack); } // eslint-disable-next-line @typescript-eslint/restrict-template-expressions const ss = String(fieldGet).split("."); if (ss.length <= 1) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const fieldResult = (record as any)[fieldGet]; return getValue(fieldResult, setPromiseBack); } const fieldResult = applyChainSafe( record, // eslint-disable-next-line @typescript-eslint/no-explicit-any (val, name) => getField(val, name, emptyFn as any), ...ss ); return getValue(fieldResult, setPromiseBack); } /** @private */ function setField>( record: T | undefined, field: F, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any ): boolean { if (record == null) { return false; } const fieldSet = isFieldAssessor(field) ? field.set : field; if ((fieldSet as never) in (record as never)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (record as any)[fieldSet] = value; } else if (typeof fieldSet === "function") { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (fieldSet as any)(record, value); } else if (typeof fieldSet === "string") { const ss = `${fieldSet}`.split("."); // eslint-disable-next-line @typescript-eslint/no-explicit-any let obj: any = record; const { length } = ss; for (let i = 0; i < length; i++) { const f = ss[i]; if (i === length - 1) { obj[f] = value; } else { obj = obj[f] || (obj[f] = {}); } } } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any (record as any)[fieldSet] = value; } return true; } /** @private */ function _getIndex(sortedIndexMap: null | number[], index: number): number { if (!sortedIndexMap) { return index; } const mapIndex = sortedIndexMap[index]; return mapIndex != null ? mapIndex : index; } export interface DataSourceParam { get: (index: number) => T; length: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any source?: any; } /** * grid data source * * @classdesc cheetahGrid.data.DataSource * @memberof cheetahGrid.data */ export class DataSource extends EventTarget implements DataSourceAPI { private _get: (index: number) => MaybePromiseOrCall; private _length: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any private readonly _source: any; protected _sortedIndexMap: null | number[] = null; static get EVENT_TYPE(): typeof EVENT_TYPE { return EVENT_TYPE; } static ofArray(array: T[]): DataSource { return new DataSource({ get: (index: number): T => array[index], length: array.length, source: array, }); } constructor(obj?: DataSourceParam | DataSource) { super(); // eslint-disable-next-line @typescript-eslint/no-explicit-any this._get = obj?.get.bind(obj) || (undefined as any); this._length = obj?.length || 0; this._source = obj?.source ?? obj; } // eslint-disable-next-line @typescript-eslint/no-explicit-any get source(): any { return this._source; } get(index: number): MaybePromiseOrUndef { return this.getOriginal(_getIndex(this._sortedIndexMap, index)); } getField>(index: number, field: F): FieldData { return this.getOriginalField(_getIndex(this._sortedIndexMap, index), field); } // eslint-disable-next-line @typescript-eslint/no-explicit-any hasField(index: number, field: FieldDef): boolean { return this.hasOriginalField(_getIndex(this._sortedIndexMap, index), field); } setField>( index: number, field: F, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any ): MaybePromise { return this.setOriginalField( _getIndex(this._sortedIndexMap, index), field, value ); } sort(field: FieldDef, order: "desc" | "asc"): MaybePromise { const sortedIndexMap = new Array(this._length); const orderFn = order !== "desc" ? ascOrderFn : descOrderFn; return sort .sortPromise( (index) => sortedIndexMap[index] != null ? sortedIndexMap[index] : (sortedIndexMap[index] = index), (index, rel) => { sortedIndexMap[index] = rel; }, this._length, orderFn, (index) => this.getOriginalField(index, field) ) .then(() => { this._sortedIndexMap = sortedIndexMap; this.fireListeners(EVENT_TYPE.UPDATED_ORDER); }); } get length(): number { return this._length; } set length(length: number) { if (this._length === length) { return; } const results = this.fireListeners(EVENT_TYPE.UPDATE_LENGTH, length); if (results.findIndex((v: unknown) => !v) >= 0) { return; } this._length = length; this.fireListeners(EVENT_TYPE.UPDATED_LENGTH, this._length); } get dataSource(): DataSource { return this; } dispose(): void { super.dispose(); } protected getOriginal(index: number): MaybePromiseOrUndef { return getValue(this._get(index), (val: PromiseCacheValue) => { this.recordPromiseCallBackInternal(index, val); }); } protected getOriginalField>( index: number, field: F ): FieldData { if (field == null) { return undefined; } const record = this.getOriginal(index); return getField(record, field, (val) => { this.fieldPromiseCallBackInternal(index, field, val); }); } protected hasOriginalField(index: number, field: FieldDef): boolean { if (field == null) { return false; } if (typeof field === "function") { return true; } const record = this.getOriginal(index); return Boolean(record && (field as never) in (record as never)); } protected setOriginalField>( index: number, field: F, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any ): MaybePromise { if (field == null) { return false; } const record = this.getOriginal(index); if (isPromise(record)) { return record.then((r) => setField(r, field, value)); } return setField(record, field, value); } protected fieldPromiseCallBackInternal>( _index: number, _field: F, // eslint-disable-next-line @typescript-eslint/no-explicit-any _value: PromiseCacheValue ): void { // } protected recordPromiseCallBackInternal( _index: number, _record: PromiseCacheValue ): void { // } // eslint-disable-next-line @typescript-eslint/no-explicit-any static EMPTY = new DataSource({ get(): void { /*noop */ }, length: 0, }); }