1/* 2 * Copyright (c) 2023-2025 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 // un-register all repeat sub-components 32 aboutToBeDeleted?: () => void; 33} 34 35interface __RepeatItemFactoryReturn<T> extends RepeatItem<T>, __IRepeatItemInternal<T> { } 36 37// implementation for existing state observation system 38class __RepeatItemPU<T> implements RepeatItem<T>, __IRepeatItemInternal<T> { 39 40 // ObservedPropertyPU is the framework class that implements @State, @Provide 41 // and App/LocalStorage properties 42 private _observedItem: ObservedPropertyPU<T>; 43 private _observedIndex?: ObservedPropertyPU<number>; 44 45 constructor(owningView: ViewPU, initialItem: T, initialIndex?: number) { 46 this._observedItem = new ObservedPropertyPU<T>(initialItem, owningView, 'Repeat item'); 47 if (initialIndex !== undefined) { 48 this._observedIndex = new ObservedPropertyPU<number>(initialIndex, owningView, 'Repeat index'); 49 } 50 } 51 52 public get item(): T { 53 return this._observedItem.get(); 54 } 55 56 public get index(): number | undefined { 57 return this._observedIndex?.get(); 58 } 59 60 public updateItem(newItemValue: T): void { 61 this._observedItem.set(newItemValue); 62 } 63 64 public updateIndex(newIndex: number): void { 65 if (!this._observedIndex?.hasDependencies()) { 66 return; 67 } 68 if (this._observedIndex?.getUnmonitored() !== newIndex) { 69 this._observedIndex?.set(newIndex); 70 } 71 } 72 73 public aboutToBeDeleted(): void { 74 this._observedItem.aboutToBeDeleted(); 75 this._observedIndex?.aboutToBeDeleted(); 76 } 77} 78 79// Framework internal, deep observation 80// Using @ObservedV2_Internal instead of @ObservedV2 to avoid forcing V2 usage. 81@ObservedV2_Internal 82class __RepeatItemV2<T> implements RepeatItem<T>, __IRepeatItemInternal<T> { 83 84 constructor(initialItem: T, initialIndex?: number) { 85 this.item = initialItem; 86 this.index = initialIndex; 87 } 88 // Using @Trace_Internal instead of @Trace to avoid forcing V2 usage. 89 @Trace_Internal item: T; 90 @Trace_Internal index?: number; 91 92 public updateItem(newItemValue: T): void { 93 this.item = newItemValue; 94 } 95 96 public updateIndex(newIndex: number): void { 97 this.index = newIndex; 98 } 99 100 public hasBindingToIndex(): boolean { 101 return this[ObserveV2.SYMBOL_REFS]?.index?.size > 0; 102 } 103} 104 105// helper, framework internal 106interface __RepeatItemInfo<T> { 107 key: string; 108 // also repeatItem includes index 109 // we need separate index because repeatItem set set and updated later than index needs to be set. 110 index: number; 111 repeatItem?: __RepeatItemFactoryReturn<T>; 112} 113 114// helper 115class __RepeatDefaultKeyGen { 116 private static weakMap_ = new WeakMap<Object | Symbol, number>(); 117 private static lastKey_ = 0; 118 119 // Return the same IDs for the same items 120 public static func<T>(item: T): string { 121 try { 122 return __RepeatDefaultKeyGen.funcImpl(item); 123 } catch (e) { 124 throw new Error(`Repeat(). Default key gen failed. Application Error!`); 125 } 126 } 127 128 // Return the same IDs for the same pairs <item, index> 129 public static funcWithIndex<T>(item: T, index: number) { 130 return `${index}__` + __RepeatDefaultKeyGen.func(item); 131 } 132 133 private static funcImpl<T>(item: T) { 134 // fast keygen logic can be used with objects/symbols only 135 if (typeof item !== 'object' && typeof item !== 'symbol') { 136 return JSON.stringify(item); 137 } 138 // generate a numeric key, store mappings in WeakMap 139 if (!this.weakMap_.has(item)) { 140 return this.weakMap_.set(item, ++this.lastKey_), `${this.lastKey_}`; 141 } 142 // use cached key 143 return `${this.weakMap_.get(item)}`; 144 } 145}; 146 147// TBD comments 148interface __RepeatConfig<T> { 149 owningView_? : ViewV2; 150 arr?: Array<T>; 151 itemGenFuncs?: { [type: string]: RepeatItemGenFunc<T> }; 152 keyGenFunc?: RepeatKeyGenFunc<T>; 153 keyGenFuncSpecified?: boolean; 154 ttypeGenFunc?: RepeatTTypeGenFunc<T>; 155 totalCountSpecified?: boolean; 156 totalCount?: number | (() => number); 157 onLazyLoading?: (index : number) => void, 158 templateOptions?: { [type: string]: RepeatTemplateImplOptions }; 159 mkRepeatItem?: (item: T, index?: number) => __RepeatItemFactoryReturn<T>; 160 onMoveHandler?: OnMoveHandler; 161 itemDragEventHandler?: ItemDragEventHandler; 162 reusable?: boolean; 163}; 164 165// should be empty string, don't change it 166const RepeatEachFuncTtype: string = ''; 167 168// __Repeat implements ForEach with child re-use for both existing state observation 169// and deep observation , for non-virtual and virtual code paths (TODO) 170class __Repeat<T> implements RepeatAPI<T> { 171 private config: __RepeatConfig<T> = {}; 172 private impl: __RepeatImpl<T> | __RepeatVirtualScrollImpl<T> | __RepeatVirtualScroll2Impl<T>; 173 private isVirtualScroll = false; 174 175 constructor(owningView: ViewV2 | ViewPU, arr: Array<T>) { 176 this.config.owningView_ = owningView instanceof ViewV2 ? owningView : undefined; 177 this.config.arr = arr ?? []; 178 this.config.itemGenFuncs = {}; 179 this.config.keyGenFunc = __RepeatDefaultKeyGen.funcWithIndex; 180 this.config.keyGenFuncSpecified = false; 181 this.config.ttypeGenFunc = undefined; 182 183 this.config.totalCountSpecified = false; 184 this.config.totalCount = this.config.arr.length; 185 this.config.templateOptions = {}; 186 this.config.reusable = true; 187 188 // to be used with ViewV2 189 const mkRepeatItemV2 = (item: T, index?: number): __RepeatItemFactoryReturn<T> => 190 new __RepeatItemV2(item as T, index); 191 192 // to be used with ViewPU 193 const mkRepeatItemPU = (item: T, index?: number): __RepeatItemFactoryReturn<T> => 194 new __RepeatItemPU(owningView as ViewPU, item, index); 195 196 const isViewV2 = (this.config.owningView_ instanceof ViewV2); 197 this.config.mkRepeatItem = isViewV2 ? mkRepeatItemV2: mkRepeatItemPU; 198 } 199 200 public each(itemGenFunc: RepeatItemGenFunc<T>): RepeatAPI<T> { 201 this.config.itemGenFuncs[RepeatEachFuncTtype] = itemGenFunc; 202 this.config.templateOptions[RepeatEachFuncTtype] = this.normTemplateOptions({}); 203 return this; 204 } 205 206 public key(keyGenFunc: RepeatKeyGenFunc<T>): RepeatAPI<T> { 207 this.config.keyGenFunc = keyGenFunc; 208 this.config.keyGenFuncSpecified = true; 209 return this; 210 } 211 212 public virtualScroll(options? : { 213 totalCount?: number, onTotalCount?: () => number, onLazyLoading?: (index: number) => void, reusable?: boolean 214 }): RepeatAPI<T> { 215 // use array length by default 216 this.config.totalCount = this.config.arr?.length; 217 this.config.totalCountSpecified = false; 218 219 // options.totalCount must be 0 or a positive integer, or undefined 220 if (Number.isInteger(options?.totalCount) && (options?.totalCount as number) >= 0) { 221 this.config.totalCount = options?.totalCount; 222 this.config.totalCountSpecified = true; 223 } 224 // available since API 18 225 if (options?.onTotalCount) { 226 this.config.totalCount = options.onTotalCount; 227 this.config.totalCountSpecified = true; 228 } 229 if (options?.onTotalCount && options?.totalCount !== undefined) { 230 stateMgmtConsole.error(`Error: Both totalCount and onTotalCount() are defined`); 231 } 232 233 if (typeof options?.reusable === 'boolean') { 234 this.config.reusable = options.reusable; 235 } else if (options?.reusable === null) { 236 this.config.reusable = true; 237 stateMgmtConsole.warn( 238 `Repeat.reusable type should be boolean. Use default setting: reusable = true`); 239 } else { 240 this.config.reusable = true; 241 } 242 if (options?.onLazyLoading) { 243 this.config.onLazyLoading = options.onLazyLoading; 244 } 245 246 this.isVirtualScroll = true; 247 return this; 248 } 249 250 // function to decide which template to use, each template has an ttype 251 public templateId(ttypeGenFunc: RepeatTTypeGenFunc<T>): RepeatAPI<T> { 252 this.config.ttypeGenFunc = ttypeGenFunc; 253 return this; 254 } 255 256 // template: ttype + builder function to render specific type of data item 257 public template(ttype: string, itemGenFunc: RepeatItemGenFunc<T>, 258 options?: RepeatTemplateOptions): RepeatAPI<T> 259 { 260 this.config.itemGenFuncs[ttype] = itemGenFunc; 261 this.config.templateOptions[ttype] = this.normTemplateOptions(options); 262 return this; 263 } 264 265 public updateArr(arr: Array<T>): RepeatAPI<T> { 266 this.config.arr = arr ?? []; 267 return this; 268 } 269 270 public render(isInitialRender: boolean): void { 271 if (!this.config.itemGenFuncs?.[RepeatEachFuncTtype]) { 272 throw new Error(`__Repeat item builder function unspecified. Usage error`); 273 } 274 if (!this.isVirtualScroll) { 275 // Repeat 276 this.impl ??= new __RepeatImpl<T>(); 277 this.impl.render(this.config, isInitialRender); 278 return; 279 } 280 // RepeatVirtualScroll v2 281 this.impl ??= new __RepeatVirtualScroll2Impl<T>(); 282 this.impl.render(this.config, isInitialRender); 283 } 284 285 // drag and drop API 286 public onMove(handler: OnMoveHandler, eventHandler?: ItemDragEventHandler): RepeatAPI<T> { 287 this.config.onMoveHandler = handler; 288 this.config.itemDragEventHandler = eventHandler; 289 return this; 290 } 291 292 // un-register all repeat sub-components 293 public aboutToBeDeleted(): void { 294 if (this.impl instanceof __RepeatImpl) { 295 this.impl.getKey2Item().forEach((itemInfo: __RepeatItemInfo<T>) => { 296 if (itemInfo.repeatItem && itemInfo.repeatItem instanceof __RepeatItemPU) { 297 itemInfo.repeatItem.aboutToBeDeleted(); 298 } 299 }); 300 } 301 } 302 303 // normalize template options 304 private normTemplateOptions(options: RepeatTemplateOptions): RepeatTemplateImplOptions { 305 const value = (options && Number.isInteger(options.cachedCount) && options.cachedCount >= 0) 306 ? { 307 cachedCount: Math.max(0, options.cachedCount), 308 cachedCountSpecified: true 309 } 310 : { 311 cachedCountSpecified: false 312 }; 313 return value; 314 } 315}; // __Repeat<T> 316