• 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 id 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};
150
151// __Repeat implements ForEach with child re-use for both existing state observation
152// and deep observation , for non-virtual and virtual code paths (TODO)
153class __Repeat<T> implements RepeatAPI<T> {
154    private config: __RepeatConfig<T> = {};
155    private impl: __RepeatImpl<T> | __RepeatVirtualScrollImpl<T>;
156    private isVirtualScroll = false;
157
158    constructor(owningView: ViewV2 | ViewPU, arr: Array<T>) {
159        this.config.owningView_ = owningView instanceof ViewV2 ? owningView : undefined;
160        this.config.arr = arr ?? [];
161        this.config.itemGenFuncs = {};
162        this.config.keyGenFunc = __RepeatDefaultKeyGen.funcWithIndex;
163        this.config.typeGenFunc = ((): string => '');
164        this.config.totalCountSpecified = false;
165        this.config.totalCount = this.config.arr.length;
166        this.config.templateOptions = {};
167
168        // to be used with ViewV2
169        const mkRepeatItemV2 = (item: T, index?: number): __RepeatItemFactoryReturn<T> =>
170            new __RepeatItemV2(item as T, index);
171
172        // to be used with ViewPU
173        const mkRepeatItemPU = (item: T, index?: number): __RepeatItemFactoryReturn<T> =>
174            new __RepeatItemPU(owningView as ViewPU, item, index);
175
176        const isViewV2 = (this.config.owningView_ instanceof ViewV2);
177        this.config.mkRepeatItem = isViewV2 ? mkRepeatItemV2 : mkRepeatItemPU;
178    }
179
180    public each(itemGenFunc: RepeatItemGenFunc<T>): RepeatAPI<T> {
181        this.config.itemGenFuncs[''] = itemGenFunc;
182        this.config.templateOptions[''] = this.normTemplateOptions({});
183        return this;
184    }
185
186    public key(keyGenFunc: RepeatKeyGenFunc<T>): RepeatAPI<T> {
187        this.config.keyGenFunc = keyGenFunc;
188        return this;
189    }
190
191    public virtualScroll(options? : { totalCount?: number }): RepeatAPI<T> {
192        if (Number.isInteger(options?.totalCount)) {
193            this.config.totalCount = options.totalCount;
194            this.config.totalCountSpecified = true;
195        } else {
196            this.config.totalCountSpecified = false;
197        }
198        this.isVirtualScroll = true;
199        return this;
200    }
201
202    // function to decide which template to use, each template has an id
203    public templateId(typeGenFunc: RepeatTypeGenFunc<T>): RepeatAPI<T> {
204        const typeGenFuncImpl = (item: T, index: number): string => {
205            try {
206                return typeGenFunc(item, index);
207            } catch (e) {
208                stateMgmtConsole.applicationError(`Repeat with virtual scroll. Exception in templateId():`, e?.message);
209                return '';
210            }
211        };
212        // typeGenFunc wrapper with ttype validation
213        const typeGenFuncSafe = (item: T, index: number): string => {
214            const itemType = typeGenFuncImpl(item, index);
215            const itemFunc = this.config.itemGenFuncs[itemType];
216            if (typeof itemFunc !== 'function') {
217                stateMgmtConsole.applicationError(`Repeat with virtual scroll. Missing Repeat.template for id '${itemType}'`);
218                return '';
219            }
220            return itemType;
221        };
222
223        this.config.typeGenFunc = typeGenFuncSafe;
224        return this;
225    }
226
227    // template: id + builder function to render specific type of data item
228    public template(type: string, itemGenFunc: RepeatItemGenFunc<T>,
229        options?: RepeatTemplateOptions): RepeatAPI<T>
230    {
231        this.config.itemGenFuncs[type] = itemGenFunc;
232        this.config.templateOptions[type] = this.normTemplateOptions(options);
233        return this;
234    }
235
236    public updateArr(arr: Array<T>): RepeatAPI<T> {
237        this.config.arr = arr ?? [];
238        return this;
239    }
240
241    public render(isInitialRender: boolean): void {
242        if (!this.config.itemGenFuncs?.['']) {
243            throw new Error(`__Repeat item builder function unspecified. Usage error`);
244        }
245        if (!this.isVirtualScroll) {
246          // Repeat
247          this.impl ??= new __RepeatImpl<T>();
248          this.impl.render(this.config, isInitialRender);
249        } else {
250          // RepeatVirtualScroll
251          this.impl ??= new __RepeatVirtualScrollImpl<T>();
252          this.impl.render(this.config, isInitialRender);
253        }
254    }
255
256    // drag and drop API
257    public onMove(handler: OnMoveHandler): RepeatAPI<T> {
258        this.config.onMoveHandler = handler;
259        return this;
260    }
261
262    // normalize template options
263    private normTemplateOptions(options: RepeatTemplateOptions): RepeatTemplateImplOptions {
264        const value = (options && Number.isInteger(options.cachedCount) && options.cachedCount >= 0)
265            ? {
266                cachedCount: Math.max(0, options.cachedCount),
267                cachedCountSpecified: true
268            }
269            : {
270                cachedCountSpecified: false
271            };
272            return value;
273    }
274}; // __Repeat<T>
275