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