tencent.jquery.pix.component 1.0.62 → 1.0.63-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,10 +19,11 @@ const DEFAULTS = {
19
19
  // 传入 $node, data, index
20
20
  updateItem: null, // 元素更新时的回调函数
21
21
  onscroll: null, // 滚动事件回调函数
22
+ shouldOccupySpace: null // 是否是静态数据的回调函数,静态数据能够占用元素
22
23
  };
23
24
 
24
25
  export function Waterfall(optionsInput = {}) {
25
- $ = getEnv();
26
+ $ = getEnv().$;
26
27
 
27
28
  this.options = Object.assign({}, DEFAULTS, optionsInput);
28
29
  const options = this.options;
@@ -30,6 +31,12 @@ export function Waterfall(optionsInput = {}) {
30
31
  // 标记是否有更新元素用的回调函数
31
32
  this.hasUpdateItem = options.updateItem && (options.updateItem.constructor === Function) ? true : false;
32
33
 
34
+ // 新方案:数据ID映射机制
35
+ this.dataIdMap = new Map(); // 数据ID -> 布局信息映射
36
+ this.nextDataId = 0; // 下一个数据ID
37
+ this.renderedDataIds = new Set(); // 已渲染的数据ID集合
38
+
39
+
33
40
  // 间隔字符串转数字
34
41
  if (options.columnGap.constructor === String) {
35
42
  // 如果是rem单位,则需要计算
@@ -85,13 +92,29 @@ export function Waterfall(optionsInput = {}) {
85
92
  this.columnItems.push(this.createColumn(i));
86
93
  }
87
94
 
88
- this.renderIndex = 0; // 渲染索引
89
- this.activeNodes = new Map(); // 当前活跃节点(索引 -> DOM)
95
+ this.renderIndex = 0; // 渲染索引(保留兼容性)
96
+ this.activeNodes = new Map(); // 当前活跃节点(数据ID -> DOM)
90
97
  this.nodePool = []; // DOM 节点池
91
98
 
99
+ // 新方案:初始化数据ID映射
100
+ this.dataIdMap = new Map(); // 数据ID -> 布局信息映射
101
+ this.nextDataId = 0; // 下一个数据ID
102
+ this.renderedDataIds = new Set(); // 已渲染的数据ID集合
103
+
104
+ // 为初始数据分配数据ID
105
+ this.options.data.forEach((item, index) => {
106
+ const dataId = index; //this.nextDataId++;
107
+ this.dataIdMap.set(dataId, {
108
+ data: true,//item,
109
+ originalIndex: index,
110
+ layoutInfo: null // 将在布局时填充
111
+ });
112
+ });
113
+
92
114
  this.init();
93
115
  }
94
116
 
117
+
95
118
  Waterfall.prototype.init = function () {
96
119
  const self = this;
97
120
  const options = this.options;
@@ -99,6 +122,8 @@ Waterfall.prototype.init = function () {
99
122
 
100
123
  this.nodePool = []; // DOM 节点池
101
124
  this.activeNodes = new Map(); // 当前活跃节点(索引 -> DOM)
125
+ this.allReadyNodes = new Map(); // 所有节点(索引 -> DOM)
126
+ this.renderIndex = 0; // 渲染索引(保留兼容性)
102
127
 
103
128
 
104
129
 
@@ -134,8 +159,9 @@ Waterfall.prototype.updateVisibleItems = function (force = false) {
134
159
  const options = this.options;
135
160
  const $container = $(options.container);
136
161
 
137
- const startTop = this.scrollTop;
162
+ const startTop = self.scrollTop; // 当前滚动位置
138
163
  const endTop = startTop + $container.height();
164
+ // console.log('startTop', startTop)
139
165
 
140
166
  // 进行可见区域的渲染更新
141
167
  this.updateCardsInView({
@@ -143,23 +169,47 @@ Waterfall.prototype.updateVisibleItems = function (force = false) {
143
169
  end: endTop,
144
170
  force
145
171
  });
172
+
146
173
  }
147
174
 
148
175
  // 新增卡片
149
- Waterfall.prototype.appendCard = function (data, index, { top, left }) {
176
+ Waterfall.prototype.appendCard = function (data, dataId, { top, left }) {
177
+ if (this.renderedDataIds.has(dataId)) {
178
+ return
179
+ }
150
180
  const self = this;
151
181
  const options = this.options;
152
182
  const $container = $(options.container);
153
183
  const $viewport = $container.find('.waterfall-list-viewport');
184
+
185
+ // 新方案:基于数据ID的数据验证
186
+ if (!this.dataIdMap.has(dataId)) {
187
+ console.error('Waterfall: Invalid dataId in appendCard', dataId);
188
+ return null;
189
+ }
190
+
191
+ const dataInfo = this.dataIdMap.get(dataId);
192
+ if (!dataInfo || !dataInfo.data) {
193
+ console.warn('Waterfall: Empty data for dataId', dataId);
194
+ }
195
+
196
+
154
197
  const $card = $(
155
198
  `<div class="waterfall-item"
156
- data-index="${index}"
157
- style="position: absolute;transform:translate(${left}px,${top}px); width:${this.columnWidth}px;"
199
+ data-index="${dataId}"
200
+ style="position: absolute;transform:translate(${left}px,${top}px);"
158
201
  >
159
- ${options.renderItem(data, index)}
202
+ ${options.renderItem(data, dataInfo.originalIndex)}
160
203
  </div> `
161
204
  );
162
205
 
206
+ this.renderedDataIds.add(dataId);
207
+
208
+ if (options.columns !== 1) {
209
+ $card.width(this.columnWidth + 'px');
210
+ }
211
+
212
+
163
213
  $viewport.append($card);
164
214
  return $card;
165
215
  }
@@ -174,49 +224,105 @@ Waterfall.prototype.updateCardsInView = function ({ start, end, force = false })
174
224
  this.createCards({ end: endBuffer });
175
225
  }
176
226
 
177
- // 基于已有的列信息,进行高度可视区域下的判定 操作已有信息。 如果是新建信息 则依赖上方的createCards方法进行创建,那么在上方会异步创建新卡片。
178
- // 阶段1:复用已有节点
227
+ const startNum = start - options.bufferHeight;
228
+ const endNum = end + options.bufferHeight;
229
+ // 新方案:基于数据ID映射机制
179
230
  const newActiveNodes = new Map();
180
231
  for (let i = 0; i < this.columns; i++) {
181
232
  const column = this.columnItems[i];
182
233
 
183
234
  for (let j = 0; j < column.children.length; j++) {
184
235
  const row = column.children[j];
185
- const renderIndex = row.renderIndex;
236
+ const dataId = row.dataId; // 使用dataId替代renderIndex
237
+
238
+ // 验证数据ID有效性
239
+ if (!this.dataIdMap.has(dataId)) {
240
+ console.warn('Waterfall: Invalid dataId detected', dataId);
241
+ continue;
242
+ }
243
+
244
+ const dataInfo = this.dataIdMap.get(dataId);
245
+
246
+ if (!dataInfo) {
247
+ console.warn('Waterfall: Invalid data for dataId', dataId);
248
+ continue;
249
+ }
250
+
251
+ const data = options.data[dataId]
252
+
253
+ // 如果当前这个节点是特殊节点 不用做处理
254
+ // 如果是特殊的静态占用元素卡片,需要指定节点不变更的数据,那么该数据的节点不能被其他数据使用
255
+ let specialNode = false;
256
+ if (options.shouldOccupySpace) {
257
+ specialNode = options.shouldOccupySpace(data) || false;
258
+ }
259
+ if (specialNode) {
260
+ continue;
261
+ }
262
+
186
263
  // 在可视区域内 进行有关卡片的操作
187
- if (row.top <= end && row.bottom >= start) {
264
+ const bool = row.top <= endNum && row.bottom >= startNum;
265
+ if (bool) {
188
266
  // 理论上什么都不动,因为卡片的位置不会变
189
- const $card = this.activeNodes.get(renderIndex);
267
+ const $card = this.activeNodes.get(dataId);
190
268
 
191
269
  let $node = null;
192
- // 如果卡片已经在DOM中,则不用更新位置
270
+
271
+ // 遍历当前的节点是否被占用,如果被占用的话,就得要从nodePool中取一个
272
+ let bool = true;
193
273
  if ($card) {
274
+ bool = hasNodeInActives(newActiveNodes, $card)
275
+ }
276
+ // 如果卡片已经在DOM中,则不用更新位置
277
+ if (bool === false) {
194
278
  $node = $card;
195
- this.activeNodes.delete(renderIndex);
279
+ this.activeNodes.delete(dataId);
280
+ // 如果是强更,这里才会采取更新
196
281
  if (force) {
197
- this.updateRenderUI($node, options.data[renderIndex], renderIndex);
282
+ this.updateRenderUI($node, data, dataId);
198
283
  }
284
+
199
285
  } else {
200
- // 卡片不在DOM中,则更新位置
201
- let nodePool = this.nodePool;
202
- if (nodePool.length === 0) {
203
- // 这里是往上方拖动时,可能需要补建的情况
204
- // 如果池子没有节点,那么进行创建
205
- $node = this.appendCard(options.data[renderIndex], renderIndex, {
206
- top: row.top, left: row.left
207
- })
208
- row.$node = $node;
286
+ const $card = this.allReadyNodes.get(dataId);
287
+ // 遍历当前的节点是否被占用,如果被占用的话,就得要从nodePool中取一个
288
+ let bool = true;
289
+ if ($card) {
290
+ bool = hasNodeInActives(newActiveNodes, $card)
291
+ }
292
+ if (bool === false) {
293
+ // 如果成功获取到card并没有占用 那么就复用这个card
294
+ $node = $card;
295
+
296
+ this.updateRenderUI($node, data, dataId);
297
+
209
298
  } else {
210
- $node = $(this.nodePool.pop());
211
- $node.css({
212
- 'transform': `translate(${row.left}px,${row.top}px)`,
213
- }).attr('data-index', renderIndex);
214
- this.updateRenderUI($node, options.data[renderIndex], renderIndex);
215
- row.$node = $node;
299
+ // 卡片不在DOM中,则更新位置
300
+ $node = getNodePoolPop(this.nodePool, newActiveNodes);
301
+ if ($node === null) {
302
+ // 这里是往上方拖动时,可能需要补建的情况
303
+ $node = this.appendCard(data, dataId, {
304
+ top: row.top, left: row.left
305
+ });
306
+ row.$node = $node;
307
+ } else {
308
+ this.updateRenderUI($node, data, dataId);
309
+ row.$node = $node;
310
+ }
216
311
  }
217
312
  }
313
+ $node.css({
314
+ 'transform': `translate(${row.left}px,${row.top}px)`,
315
+ }).attr('data-index', dataId);
218
316
 
219
- newActiveNodes.set(renderIndex, $node);
317
+ newActiveNodes.set(dataId, $node);
318
+
319
+ // 清除掉在NodePool中的card
320
+ const index = this.nodePool.indexOf($card);
321
+ if (index !== -1) {
322
+ this.nodePool.splice(index, 1);
323
+ }
324
+
325
+ // console.log('302-row', row.dataId, dataInfo);
220
326
  }
221
327
  }
222
328
  }
@@ -224,11 +330,16 @@ Waterfall.prototype.updateCardsInView = function ({ start, end, force = false })
224
330
  // 阶段2:处理不活跃节点
225
331
  this.activeNodes.forEach($node => {
226
332
  $node.css('transform', `translateY(-9999px)`);// 移出可视区域
227
- this.nodePool.push($node);
333
+ if (this.nodePool.indexOf($node) === -1) {
334
+ this.nodePool.push($node);
335
+ }
228
336
  });
229
337
  this.activeNodes = newActiveNodes;
338
+ // console.log('this.activeNodes', this.activeNodes);
339
+ // console.log('this.nodePool', this.nodePool);
230
340
  }
231
341
 
342
+
232
343
  Waterfall.prototype.getMaxHeight = function () {
233
344
  let maxHeight = 0;
234
345
  for (let i = 0; i < this.columns; i++) {
@@ -276,79 +387,127 @@ Waterfall.prototype.getMinHeightColumn = function () {
276
387
  }
277
388
 
278
389
  // 创建卡片
279
- Waterfall.prototype.createCards = function ({ end }) {
390
+ Waterfall.prototype.createCards = function ({ end, dataId = -1 }) {
280
391
  const self = this;
281
392
  const options = this.options;
282
393
  const $container = $(options.container);
283
- const renderIndex = this.renderIndex;
284
394
 
285
- if (renderIndex >= options.data.length) {
395
+ // 新方案:获取下一个未渲染的数据ID
396
+ let nextDataId = null;
397
+ for (let [dataId, dataInfo] of this.dataIdMap) {
398
+ if (!this.renderedDataIds.has(dataId)) {
399
+ nextDataId = dataId;
400
+ break;
401
+ }
402
+ }
403
+
404
+
405
+ // 如果没有更多数据需要渲染
406
+ if (nextDataId === null) {
286
407
  const maxHeight = this.getMaxHeight();
287
- // 设置一次整体高度
288
- $(options.container).find('.waterfall-list-scroll').css('height', maxHeight + options.marginBottom)
408
+ $container.find('.waterfall-list-scroll').css('height', maxHeight + options.marginBottom + 'px');
289
409
  return;
290
410
  }
291
411
 
412
+ const dataInfo = this.dataIdMap.get(nextDataId);
413
+ if (!dataInfo || !dataInfo.data) {
414
+ console.warn('Waterfall: Invalid data for dataId', nextDataId);
415
+ this.renderedDataIds.add(nextDataId);
416
+ return;
417
+ }
418
+
419
+ if (this.renderIndex >= options.data.length) {
420
+ const maxHeight = this.getMaxHeight();
421
+ $container.find('.waterfall-list-scroll').css('height', maxHeight + options.marginBottom + 'px');
422
+ return
423
+ }
424
+
425
+ const data = options.data[nextDataId];
426
+
292
427
  let column = this.getMinHeightColumn();
293
428
  if (column === null) {
294
- // 没有可用的列,则在第一列创建,说明此时还没有数据
295
429
  column = this.columnItems[0];
296
430
  }
297
431
 
298
432
  const top = column.bottom === 0 ? options.marginTop : (column.bottom + options.rowGap);
433
+ const position = { top, left: column.left };
434
+ const row = createDefaultRow(position);
299
435
 
300
- // 设置卡片位置
301
- const position = {
302
- top,
303
- left: column.left,
304
- }
436
+ this.renderIndex += 1;
305
437
 
306
- const row = createDefaultRow(position);
438
+ let specialNode = false;
307
439
 
440
+ // 如果是特殊的卡片,需要指定节点不变更的数据,那么该数据的节点不能被其他数据使用
441
+ if (options.shouldOccupySpace) {
442
+ specialNode = options.shouldOccupySpace(data) || false;
443
+ }
308
444
 
309
- // 添加卡片
445
+ // 添加卡片,使用dataId作为唯一标识
310
446
  let $card = null;
311
- if (this.nodePool.length === 0) {
312
- $card = this.appendCard(options.data[renderIndex], renderIndex, position);
447
+ if (this.nodePool.length === 0 || specialNode === true) {
448
+ $card = this.appendCard(data, nextDataId, position);
313
449
  } else {
314
- $card = $(this.nodePool.pop());
315
- $card.css({
316
- 'transform': `translate(${row.left}px,${row.top}px)`,
317
- }).attr('data-index', renderIndex);
318
- this.updateRenderUI($card, options.data[renderIndex], renderIndex);
319
- }
450
+ const $tmp = getNodePoolPop(this.nodePool, this.activeNodes);
451
+ if ($tmp) {
452
+ $card = $tmp;
453
+ $card.css({
454
+ 'transform': `translate(${row.left}px,${row.top}px)`,
455
+ }).attr('data-index', nextDataId);
456
+ this.updateRenderUI($card, data, nextDataId);
457
+ } else {
458
+ $card = this.appendCard(data, nextDataId, position);
459
+ }
320
460
 
461
+ }
321
462
 
322
463
  row.$node = $card;
464
+ row.dataId = nextDataId; // 使用dataId替代renderIndex
465
+ if (dataId !== -1) {
466
+ row.dataId = dataId;
467
+ }
468
+
469
+ // 记录布局信息
470
+ // dataInfo.layoutInfo = {
471
+ // //columnIndex: this.columnItems.indexOf(column),
472
+ // //position: position,
473
+ // // row: row
474
+ // };
475
+
323
476
 
324
- // 把新增的卡片放进 activeNodes, 当前是 展示状态的
325
- this.activeNodes.set(renderIndex, $card);
326
477
 
327
478
 
479
+ if (specialNode === false) {
480
+ // 把新增的卡片放进 activeNodes 当成活跃节点元素,那么是 可以动态使用的
481
+ this.activeNodes.set(nextDataId, $card);
482
+
483
+ this.allReadyNodes.set(nextDataId, $card);
484
+ } else {
485
+ // 如果是特殊的,这里不要记录了
486
+ this.allReadyNodes.set(nextDataId, null);
487
+ }
488
+
489
+ this.renderedDataIds.add(nextDataId);
490
+
328
491
  // 更新列的底部距离
329
492
  column.bottom = top + $card.height();
330
493
  column.children.push(row);
331
-
332
- // 计算当前卡片的位置
333
494
  row.bottom = column.bottom;
334
- row.renderIndex = renderIndex;
335
-
336
- this.renderIndex += 1;
337
-
338
- let hasNextData = this.renderIndex < options.data.length;
339
495
 
496
+ // 检查是否需要继续创建卡片
340
497
  const minHeight = this.getMinHeight();
341
- if (hasNextData && (minHeight < end)) {
498
+ const hasMoreData = this.renderedDataIds.size < this.dataIdMap.size;
499
+
500
+ if (hasMoreData && (minHeight < end)) {
342
501
  window.requestAnimationFrame(() => {
343
502
  this.createCards({ end });
344
503
  });
345
504
  } else {
346
505
  const maxHeight = this.getMaxHeight();
347
- // 设置一次整体高度
348
- $(options.container).find('.waterfall-list-scroll').css('height', maxHeight + options.marginBottom)
506
+ $(options.container).find('.waterfall-list-scroll').css('height', maxHeight + options.marginBottom + 'px');
349
507
  }
350
508
  }
351
509
 
510
+
352
511
  Waterfall.prototype.createColumn = function (index) {
353
512
  const res = {
354
513
  left: 0,
@@ -365,33 +524,137 @@ Waterfall.prototype.createColumn = function (index) {
365
524
  return res;
366
525
  }
367
526
 
368
- Waterfall.prototype.updateRenderUI = function ($node, data, index) {
527
+ Waterfall.prototype.updateRenderUI = function ($node, data, dataId) {
369
528
  const options = this.options;
529
+
530
+ // 新方案:基于数据ID的数据验证
531
+ if (!this.dataIdMap.has(dataId)) {
532
+ console.error('Waterfall: Invalid dataId in updateRenderUI', dataId);
533
+ return;
534
+ }
535
+
536
+ const dataInfo = this.dataIdMap.get(dataId);
537
+ if (!dataInfo || !dataInfo.data) {
538
+ console.warn('Waterfall: Empty data for dataId', dataId);
539
+ }
540
+
370
541
  if (this.hasUpdateItem === true) {
371
- options.updateItem($node, data, index)
542
+ options.updateItem($node, data, dataInfo.originalIndex)
372
543
  } else {
373
- $node.html(options.renderItem(data, index));
544
+ $node.html(options.renderItem(data, dataInfo.originalIndex));
374
545
  }
375
546
  }
376
547
 
548
+
549
+
377
550
  Waterfall.prototype.updateData = function (newData) {
378
- this.options.data = newData;
551
+ const options = this.options;
552
+ options.data = newData;
553
+
554
+ // 新方案:重新建立数据ID映射
555
+ //this.dataIdMap.clear();
556
+ //this.renderedDataIds.clear();
557
+ //this.nextDataId = 0;
558
+
559
+ // 为每个数据项分配唯一ID
560
+ options.data.forEach((item, index) => {
561
+ const dataId = index; // this.nextDataId++;
562
+ if (!this.allReadyNodes.has(dataId)) {
563
+ this.dataIdMap.set(dataId, {
564
+ data: true,// item,
565
+ originalIndex: index,
566
+ layoutInfo: null // 将在布局时填充
567
+ });
568
+ // 如果没有准备好这个数据,这里要创建一个占位节点
569
+ this.createCards({ end: 0, dataId });
570
+ }
571
+ });
572
+
379
573
  this.updateVisibleItems(true); // 强制更新渲染
574
+
575
+ // 重新计算所有卡片位置并更新位置
576
+ // this.updatePointCards();
577
+ }
578
+
579
+
580
+
581
+ // 重新计算设置一遍所有的卡片位置
582
+ Waterfall.prototype.updatePointCards = function () {
583
+ // 有问题 不要用
584
+ return
585
+ const self = this;
586
+ const options = this.options;
587
+ const columnItems = this.columnItems;
588
+ let top = options.marginTop;
589
+ for (let i = 0; i < columnItems.length; i++) {
590
+ const column = columnItems[i];
591
+ // 这里为了简化,各列的瀑布流保持不动,只更新各列下节点的top位置,不做节点的跨列位移
592
+ for (let j = 0; j < column.children.length; j++) {
593
+ const row = column.children[j];
594
+ const $card = row.$node;
595
+
596
+ // 验证数据ID有效性
597
+ if (!this.dataIdMap.has(row.dataId)) {
598
+ console.warn('Waterfall: Invalid dataId in updatePointCards', row.dataId);
599
+ continue;
600
+ }
601
+
602
+ // 第一个的top不需要更新
603
+ if (j === 0) {
604
+ row.bottom = top + $card.height();
605
+ } else {
606
+ row.top = column.children[j - 1].bottom + options.rowGap;
607
+ row.bottom = row.top + $card.height();
608
+ // 更新卡片位置
609
+ // $card.css({
610
+ // 'transform': `translate(${row.left}px,${row.top}px)`,
611
+ // })
612
+ }
613
+
614
+ }
615
+ // 设置一次该列的bottom
616
+ column.bottom = getBottomByColumn(column);
617
+ }
380
618
  }
381
619
 
620
+
382
621
  function createDefaultRow({ top, left }) {
383
622
  return {
384
623
  top,
385
624
  left,
386
625
  bottom: 0,
387
626
  $node: null,
388
- renderIndex: -1
627
+ dataId: -1 // 新方案:使用dataId替代renderIndex
389
628
  }
390
629
  }
391
630
 
392
- function getBottomByColumn(rows) {
393
- if (rows.children.length === 0) {
631
+
632
+ function getBottomByColumn(column) {
633
+ if (column.children.length === 0) {
394
634
  return 0;
395
635
  }
396
- return rows[rows.length - 1].bottom;
636
+ const child = column.children;
637
+ return child[child.length - 1].bottom;
638
+ }
639
+
640
+ function hasNodeInActives(mapObj, $node) {
641
+ let bool = false
642
+ mapObj.forEach((item) => {
643
+ if (item === $node) {
644
+ bool = true;
645
+ }
646
+ });
647
+ return bool;
648
+ }
649
+
650
+ // 从众多jq对象数组取得一个不重复的
651
+ function getNodePoolPop(nodePool, actives) {
652
+ for (let i = 0; i < nodePool.length; i++) {
653
+ const $node = nodePool[i];
654
+ if (!hasNodeInActives(actives, $node)) {
655
+ nodePool.splice(i, 1);
656
+ return $node;
657
+ }
658
+ }
659
+ return null;
397
660
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tencent.jquery.pix.component",
3
- "version": "1.0.62",
3
+ "version": "1.0.63-beta.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "files": [