• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023-2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 *
15 * all definitions in this file are framework internal
16*/
17
18// declare add. functions beyond that framework needs internally
19interface __IRepeatItemInternal<T> {
20    // set new item value, used during Repeat.each update when
21    // - array item has been replaced with new value (LazyForEach onDataChanged)
22    // - on child reuse. reuse children to render newItemValue
23    updateItem: (newItemVal: T) => void;
24
25    // set new index value, used during  Repeat.each update when
26    // - order of item in array has changed  (LazyforEach onDataMoved)
27    // - on child reuse. reuse children to render newItemValue. index of
28    //   newItemValue is a new one
29    updateIndex: (newIndexValue: number) => void;
30}
31
32interface __RepeatItemFactoryReturn<T> extends RepeatItem<T>, __IRepeatItemInternal<T> { }
33
34// implementation for existing state observation system
35class __RepeatItemPU<T> implements RepeatItem<T>, __IRepeatItemInternal<T> {
36
37    // ObservedPropertyPU is the framework class that implements @State, @Provide
38    // and App/LocalStorage properties
39    private _observedItem: ObservedPropertyPU<T>;
40    private _observedIndex?: ObservedPropertyPU<number>;
41
42    constructor(owningView: ViewPU, initialItem: T, initialIndex?: number) {
43        this._observedItem = new ObservedPropertyPU<T>(initialItem, owningView, 'Repeat item');
44        if (initialIndex !== undefined) {
45            this._observedIndex = new ObservedPropertyPU<number>(initialIndex, owningView, 'Repeat index');
46        }
47    }
48
49    public get item(): T {
50        return this._observedItem.get();
51    }
52
53    public get index(): number | undefined {
54        return this._observedIndex?.get();
55    }
56
57    public updateItem(newItemValue: T): void {
58        this._observedItem.set(newItemValue);
59    }
60
61    public updateIndex(newIndex: number): void {
62        if (!this._observedIndex?.hasDependencies()) {
63            return;
64        }
65        if (this._observedIndex?.getUnmonitored() !== newIndex) {
66            this._observedIndex?.set(newIndex);
67        }
68    }
69}
70
71// Framework internal, deep observation
72// Using @ObservedV2_Internal instead of @ObservedV2 to avoid forcing V2 usage.
73@ObservedV2_Internal
74class __RepeatItemV2<T> implements RepeatItem<T>, __IRepeatItemInternal<T> {
75
76    constructor(initialItem: T, initialIndex?: number) {
77        this.item = initialItem;
78        this.index = initialIndex;
79    }
80    // Using @Trace_Internal instead of @Trace to avoid forcing V2 usage.
81    @Trace_Internal item: T;
82    @Trace_Internal index?: number;
83
84    public updateItem(newItemValue: T): void {
85        this.item = newItemValue;
86    }
87
88    public updateIndex(newIndex: number): void {
89        if (this.index !== undefined) {
90            this.index = newIndex;
91        }
92    }
93}
94
95// helper, framework internal
96interface __RepeatItemInfo<T> {
97    key: string;
98    // also repeatItem includes index
99    // we need separate index because repeatItem set set and updated later than index needs to be set.
100    index: number;
101    repeatItem?: __RepeatItemFactoryReturn<T>;
102}
103
104// helper
105class __RepeatDefaultKeyGen {
106    private static weakMap_ = new WeakMap<Object | Symbol, number>();
107    private static lastKey_ = 0;
108
109    // Return the same IDs for the same items
110    public static func<T>(item: T): string {
111        try {
112            return __RepeatDefaultKeyGen.funcImpl(item);
113        } catch (e) {
114            throw new Error(`Repeat(). Default key gen failed. Application Error!`);
115        }
116    }
117
118    // Return the same IDs for the same pairs <item, index>
119    public static funcWithIndex<T>(item: T, index: number) {
120        return `${index}__` + __RepeatDefaultKeyGen.func(item);
121    }
122
123    private static funcImpl<T>(item: T) {
124        // fast keygen logic can be used with objects/symbols only
125        if (typeof item !== 'object' && typeof item !== 'symbol') {
126            return JSON.stringify(item);
127        }
128        // generate a numeric key, store mappings in WeakMap
129        if (!this.weakMap_.has(item)) {
130            return this.weakMap_.set(item, ++this.lastKey_), `${this.lastKey_}`;
131        }
132        // use cached key
133        return `${this.weakMap_.get(item)}`;
134    }
135};
136
137// TBD comments
138interface __RepeatConfig<T> {
139    owningView_? : ViewV2;
140    arr?: Array<T>;
141    itemGenFuncs?: { [type: string]: RepeatItemGenFunc<T> };
142    keyGenFunc?: RepeatKeyGenFunc<T>;
143    typeGenFunc?: RepeatTypeGenFunc<T>;
144    totalCountSpecified?: boolean;
145    totalCount?: number;
146    templateOptions?: { [type: string]: RepeatTemplateImplOptions };
147    mkRepeatItem?: (item: T, index?: number) => __RepeatItemFactoryReturn<T>;
148    onMoveHandler?: OnMoveHandler;
149    itemDragEventHandler?: ItemDragEventHandler;
150    reusable?: boolean;
151};
152
153// __Repeat implements ForEach with child re-use for both existing state observation
154// and deep observation , for non-virtual and virtual code paths (TODO)
155class __Repeat<T> implements RepeatAPI<T> {
156    private config: __RepeatConfig<T> = {};
157    private impl: __RepeatImpl<T> | __RepeatVirtualScrollImpl<T>;
158    private isVirtualScroll = false;
159
160    constructor(owningView: ViewV2 | ViewPU, arr: Array<T>) {
161        this.config.owningView_ = owningView instanceof ViewV2 ? owningView : undefined;
162        this.config.arr = arr ?? [];
163        this.config.itemGenFuncs = {};
164        this.config.keyGenFunc = __RepeatDefaultKeyGen.funcWithIndex;
165        this.config.typeGenFunc = ((): string => '');
166        this.config.totalCountSpecified = false;
167        this.config.totalCount = this.config.arr.length;
168        this.config.templateOptions = {};
169        this.config.reusable = true;
170
171        // to be used with ViewV2
172        const mkRepeatItemV2 = (item: T, index?: number): __RepeatItemFactoryReturn<T> =>
173            new __RepeatItemV2(item as T, index);
174
175        // to be used with ViewPU
176        const mkRepeatItemPU = (item: T, index?: number): __RepeatItemFactoryReturn<T> =>
177            new __RepeatItemPU(owningView as ViewPU, item, index);
178
179        const isViewV2 = (this.config.owningView_ instanceof ViewV2);
180        this.config.mkRepeatItem = isViewV2 ? mkRepeatItemV2: mkRepeatItemPU;
181    }
182
183    public each(itemGenFunc: RepeatItemGenFunc<T>): RepeatAPI<T> {
184        this.config.itemGenFuncs[''] = itemGenFunc;
185        this.config.templateOptions[''] = this.normTemplateOptions({});
186        return this;
187    }
188
189    public key(keyGenFunc: RepeatKeyGenFunc<T>): RepeatAPI<T> {
190        this.config.keyGenFunc = keyGenFunc;
191        return this;
192    }
193
194    public virtualScroll(options? : { totalCount?: number, reusable?: boolean }): RepeatAPI<T> {
195        if (Number.isInteger(options?.totalCount)) {
196            this.config.totalCount = options.totalCount;
197            this.config.totalCountSpecified = true;
198        } else {
199            this.config.totalCountSpecified = false;
200        }
201        this.isVirtualScroll = true;
202
203        if (typeof options?.reusable === 'boolean') {
204            this.config.reusable = options.reusable;
205        } else if (options?.reusable === null) {
206            this.config.reusable = true;
207            stateMgmtConsole.warn(
208                `Repeat.reusable type should be boolean. Use default setting: reusable = true`);
209        } else {
210            this.config.reusable = true;
211        }
212        return this;
213    }
214
215    // function to decide which template to use, each template has an ttype
216    public templateId(typeGenFunc: RepeatTypeGenFunc<T>): RepeatAPI<T> {
217        const typeGenFuncImpl = (item: T, index: number): string => {
218            try {
219                return typeGenFunc(item, index);
220            } catch (e) {
221                stateMgmtConsole.applicationError(`Repeat with virtual scroll. Exception in templateId():`, e?.message);
222                return '';
223            }
224        };
225        // typeGenFunc wrapper with ttype validation
226        const typeGenFuncSafe = (item: T, index: number): string => {
227            const itemType = typeGenFuncImpl(item, index);
228            const itemFunc = this.config.itemGenFuncs[itemType];
229            if (typeof itemFunc !== 'function') {
230                stateMgmtConsole.applicationError(`Repeat with virtual scroll. Missing Repeat.template for ttype '${itemType}'`);
231                return '';
232            }
233            return itemType;
234        };
235
236        this.config.typeGenFunc = typeGenFuncSafe;
237        return this;
238    }
239
240    // template: ttype + builder function to render specific type of data item
241    public template(type: string, itemGenFunc: RepeatItemGenFunc<T>,
242        options?: RepeatTemplateOptions): RepeatAPI<T>
243    {
244        this.config.itemGenFuncs[type] = itemGenFunc;
245        this.config.templateOptions[type] = this.normTemplateOptions(options);
246        return this;
247    }
248
249    public updateArr(arr: Array<T>): RepeatAPI<T> {
250        this.config.arr = arr ?? [];
251        return this;
252    }
253
254    public render(isInitialRender: boolean): void {
255        if (!this.config.itemGenFuncs?.['']) {
256            throw new Error(`__Repeat item builder function unspecified. Usage error`);
257        }
258        if (!this.isVirtualScroll) {
259          // Repeat
260          this.impl ??= new __RepeatImpl<T>();
261          this.impl.render(this.config, isInitialRender);
262        } else {
263          // RepeatVirtualScroll
264          this.impl ??= new __RepeatVirtualScrollImpl<T>();
265          this.impl.render(this.config, isInitialRender);
266        }
267    }
268
269    // drag and drop API
270    public onMove(handler: OnMoveHandler, eventHandler?: ItemDragEventHandler): RepeatAPI<T> {
271        this.config.onMoveHandler = handler;
272        this.config.itemDragEventHandler = eventHandler;
273        return this;
274    }
275
276    // normalize template options
277    private normTemplateOptions(options: RepeatTemplateOptions): RepeatTemplateImplOptions {
278        const value = (options && Number.isInteger(options.cachedCount) && options.cachedCount >= 0)
279            ? {
280                cachedCount: Math.max(0, options.cachedCount),
281                cachedCountSpecified: true
282            }
283            : {
284                cachedCountSpecified: false
285            };
286            return value;
287    }
288}; // __Repeat<T>
289