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