tencent.jquery.pix.component 1.0.83 → 1.0.86
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.
|
@@ -0,0 +1,1434 @@
|
|
|
1
|
+
import { remToPx } from "../../utils/utils.js";
|
|
2
|
+
import { getEnv } from "../config.js";
|
|
3
|
+
import "./waterfallv2.scss";
|
|
4
|
+
|
|
5
|
+
let $ = null;
|
|
6
|
+
|
|
7
|
+
// 全局自增 ID 计数器,用于为没有 dataId 的数据元素统一分配唯一 ID
|
|
8
|
+
let _globalDataIdCounter = 0;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 为数据列表中没有 dataId 的元素统一分配唯一 ID
|
|
12
|
+
* 规则:遍历所有元素,若某个元素缺少 dataId 属性,则统一为整个列表重新分配
|
|
13
|
+
* @param {Array} data - 数据列表
|
|
14
|
+
*/
|
|
15
|
+
function assignDataIds(data) {
|
|
16
|
+
// 检查是否有元素缺少 dataId
|
|
17
|
+
const hasMissing = data.some(item => item.dataId === undefined || item.dataId === null);
|
|
18
|
+
if (!hasMissing) return; // 全部已有 dataId,无需处理
|
|
19
|
+
|
|
20
|
+
// 存在缺失,统一为所有元素分配 dataId
|
|
21
|
+
console.warn('Waterfallv2: 数据列表中存在未定义 dataId 的元素,将由组件统一分配。');
|
|
22
|
+
data.forEach(item => {
|
|
23
|
+
if (item.dataId === undefined || item.dataId === null) {
|
|
24
|
+
item.dataId = 'pix_wf_' + (_globalDataIdCounter++);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 默认配置
|
|
30
|
+
const DEFAULTS = {
|
|
31
|
+
columns: 2, // 列数
|
|
32
|
+
columnGap: 10, // 左右间隔
|
|
33
|
+
rowGap: 12, // 上下间隔
|
|
34
|
+
marginTop: 0, // 距离顶部距离
|
|
35
|
+
marginBottom: 12, // 最后一行距离底部距离
|
|
36
|
+
bufferHeight: '2rem', // 缓冲高度,进行可能的预先添加高度
|
|
37
|
+
startPoints: [], // 起始点距离
|
|
38
|
+
data: [], // 数据源
|
|
39
|
+
container: '', // 容器元素
|
|
40
|
+
renderItem(data, index, $card) { // 元素首次渲染时的回调函数, 如果把updateItem设置为空,那么更新时则会兜底触发renderItem
|
|
41
|
+
return '<div class="waterfallv2-item"></div>';
|
|
42
|
+
},
|
|
43
|
+
scrollDom: null, // 滚动元素,如果传入了滚动元素,那么用来计算的窗口高度就以滚动元素的高度为准
|
|
44
|
+
// 传入 $node, data, index
|
|
45
|
+
updateItem: null, // 元素更新时的回调函数
|
|
46
|
+
onscroll: null, // 滚动事件回调函数
|
|
47
|
+
shouldOccupySpace: null, // 是否是静态数据的回调函数,静态数据能够占用元素
|
|
48
|
+
showLoading: null, // 展示loading的回调函数 params:$node
|
|
49
|
+
hideLoading: null, // 隐藏loading的回调函数
|
|
50
|
+
createLoading: null, // 创建loading的回调函数
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function Waterfallv2(optionsInput = {}) {
|
|
54
|
+
$ = getEnv().$;
|
|
55
|
+
|
|
56
|
+
this.optionsInput = optionsInput
|
|
57
|
+
|
|
58
|
+
this.init();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
Waterfallv2.prototype.init = function (optionsMain = null) {
|
|
63
|
+
// 重置自增的基础数值
|
|
64
|
+
_globalDataIdCounter = 0;
|
|
65
|
+
if (optionsMain) {
|
|
66
|
+
this.options = Object.assign({}, DEFAULTS, optionsMain);
|
|
67
|
+
} else {
|
|
68
|
+
this.options = Object.assign({}, DEFAULTS, this.optionsInput);
|
|
69
|
+
}
|
|
70
|
+
const self = this;
|
|
71
|
+
const options = this.options;
|
|
72
|
+
const $container = $(options.container);
|
|
73
|
+
const $scrollDom = options.scrollDom ? $(options.scrollDom) : $container;
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
// 标记是否有更新元素用的回调函数
|
|
77
|
+
this.hasUpdateItem = options.updateItem && (options.updateItem.constructor === Function) ? true : false;
|
|
78
|
+
|
|
79
|
+
// 数据ID映射机制
|
|
80
|
+
this.dataIdMap = new Map(); // dataId -> 布局信息映射
|
|
81
|
+
this.renderedDataIds = new Set(); // 已渲染的 dataId 集合
|
|
82
|
+
|
|
83
|
+
this.$loadingNode = null;
|
|
84
|
+
|
|
85
|
+
this.isShowLoading = false; // 是否展示loading
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
// 间隔字符串转数字
|
|
89
|
+
if (options.columnGap.constructor === String) {
|
|
90
|
+
if (options.columnGap.indexOf('rem') > -1) {
|
|
91
|
+
options.columnGap = remToPx(options.columnGap);
|
|
92
|
+
} else {
|
|
93
|
+
options.columnGap = parseFloat(options.columnGap);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (options.rowGap.constructor === String) {
|
|
97
|
+
if (options.rowGap.indexOf('rem') > -1) {
|
|
98
|
+
options.rowGap = remToPx(options.rowGap);
|
|
99
|
+
} else {
|
|
100
|
+
options.rowGap = parseFloat(options.rowGap);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (options.bufferHeight.constructor === String) {
|
|
104
|
+
if (options.bufferHeight.indexOf('rem') > -1) {
|
|
105
|
+
options.bufferHeight = remToPx(options.bufferHeight);
|
|
106
|
+
} else {
|
|
107
|
+
options.bufferHeight = parseFloat(options.bufferHeight);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (options.marginTop.constructor === String) {
|
|
111
|
+
if (options.marginTop.indexOf('rem') > -1) {
|
|
112
|
+
options.marginTop = remToPx(options.marginTop);
|
|
113
|
+
} else {
|
|
114
|
+
options.marginTop = parseFloat(options.marginTop);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (options.marginBottom.constructor === String) {
|
|
118
|
+
if (options.marginBottom.indexOf('rem') > -1) {
|
|
119
|
+
options.marginBottom = remToPx(options.marginBottom);
|
|
120
|
+
} else {
|
|
121
|
+
options.marginBottom = parseFloat(options.marginBottom);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
options.startPoints.forEach((item, index) => {
|
|
125
|
+
if (item.constructor === String) {
|
|
126
|
+
options.startPoints[index] = remToPx(item);
|
|
127
|
+
} else {
|
|
128
|
+
item = parseFloat(item);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const allWidth = $container.width();
|
|
133
|
+
this.allWidth = allWidth;
|
|
134
|
+
// 计算每列的宽度
|
|
135
|
+
this.columnWidth = (allWidth - (options.columns - 1) * options.columnGap) / options.columns;
|
|
136
|
+
|
|
137
|
+
// 记录列数
|
|
138
|
+
this.columns = options.columns;
|
|
139
|
+
|
|
140
|
+
this.columnItems = []; // 列元素数组
|
|
141
|
+
for (let i = 0; i < this.columns; i++) {
|
|
142
|
+
this.columnItems.push(this.createColumn(i));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.$scrollDom = $scrollDom;
|
|
146
|
+
|
|
147
|
+
this.nodePool = []; // DOM 节点池
|
|
148
|
+
this.activeNodes = new Map(); // 当前活跃节点(索引 -> DOM)
|
|
149
|
+
this.allReadyNodes = new Map(); // 所有节点(索引 -> DOM)
|
|
150
|
+
this.renderIndex = 0; // 渲染索引(保留兼容性)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
// 为数据列表中缺少 dataId 的元素统一分配唯一 ID
|
|
155
|
+
assignDataIds(this.options.data);
|
|
156
|
+
|
|
157
|
+
// 数据ID映射:dataId -> 布局信息
|
|
158
|
+
this.dataIdMap = new Map();
|
|
159
|
+
this.renderedDataIds = new Set(); // 已渲染的 dataId 集合
|
|
160
|
+
|
|
161
|
+
// 为初始数据建立 dataIdMap
|
|
162
|
+
this.options.data.forEach((item) => {
|
|
163
|
+
const dataId = item.dataId;
|
|
164
|
+
this.dataIdMap.set(dataId, {
|
|
165
|
+
data: true,
|
|
166
|
+
layoutInfo: null // 将在布局时填充
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
$container.html(`
|
|
171
|
+
<div class="waterfallv2-list-scroll" style="">
|
|
172
|
+
<div class="Waterfallv2-list-viewport" style="position:relative;"></div>
|
|
173
|
+
</div>
|
|
174
|
+
`);
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
// 如果有定义loading函数 那么创建一个loading节点元素
|
|
179
|
+
console.log('options.createLoading', options.createLoading)
|
|
180
|
+
if (options.createLoading) {
|
|
181
|
+
this.$loadingNode = $(
|
|
182
|
+
`<div class="waterfallv2-loading" style="position:absolute;top:0;left:0;width:100%;transform: translate(0px, -99999px)"></div>`
|
|
183
|
+
);
|
|
184
|
+
$container.find('.Waterfallv2-list-viewport').append(this.$loadingNode);
|
|
185
|
+
|
|
186
|
+
options.createLoading(this.$loadingNode);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 绑定滚动事件(节流处理)
|
|
190
|
+
$scrollDom.off().on('scroll', function () {
|
|
191
|
+
self.scrollTop = $(this).scrollTop();
|
|
192
|
+
|
|
193
|
+
window.requestAnimationFrame(() => {
|
|
194
|
+
self.updateVisibleItems();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (options.onscroll && options.onscroll.constructor === Function) {
|
|
198
|
+
options.onscroll(this, self.scrollTop);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
this.scrollTop = $scrollDom.scrollTop(); // 当前滚动位置
|
|
203
|
+
|
|
204
|
+
// 首次渲染
|
|
205
|
+
self.updateVisibleItems();
|
|
206
|
+
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// force 强制更新渲染
|
|
210
|
+
Waterfallv2.prototype.updateVisibleItems = function (force = false) {
|
|
211
|
+
const self = this;
|
|
212
|
+
const options = this.options;
|
|
213
|
+
let h = 0;
|
|
214
|
+
if (options.scrollDom) {
|
|
215
|
+
h = $(options.scrollDom).height();
|
|
216
|
+
} else {
|
|
217
|
+
h = $(options.container).height();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const startTop = self.scrollTop; // 当前滚动位置
|
|
221
|
+
const endTop = startTop + h;
|
|
222
|
+
|
|
223
|
+
// 进行可见区域的渲染更新
|
|
224
|
+
this.updateCardsInView({
|
|
225
|
+
start: startTop,
|
|
226
|
+
end: endTop,
|
|
227
|
+
force
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 新增卡片
|
|
233
|
+
Waterfallv2.prototype.appendCard = function (data, dataId, { top, left }) {
|
|
234
|
+
const options = this.options;
|
|
235
|
+
const $container = $(options.container);
|
|
236
|
+
const $viewport = $container.find('.Waterfallv2-list-viewport');
|
|
237
|
+
|
|
238
|
+
if (!this.dataIdMap.has(dataId)) {
|
|
239
|
+
console.error('Waterfallv2: Invalid dataId in appendCard', dataId);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 获取该数据在 options.data 中的当前索引
|
|
244
|
+
const originalIndex = options.data.indexOf(data);
|
|
245
|
+
|
|
246
|
+
const $card = $(
|
|
247
|
+
`<div class="waterfallv2-item"
|
|
248
|
+
data-index="${dataId}"
|
|
249
|
+
style="position: absolute;transform:translate(${left}px,${top}px);"
|
|
250
|
+
>
|
|
251
|
+
</div> `
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
$viewport.append($card);
|
|
255
|
+
|
|
256
|
+
const str = options.renderItem(data, originalIndex, $card);
|
|
257
|
+
|
|
258
|
+
if (str) {
|
|
259
|
+
$card.html(str);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.renderedDataIds.add(dataId);
|
|
263
|
+
|
|
264
|
+
if (options.columns !== 1) {
|
|
265
|
+
$card.width(this.columnWidth + 'px');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return $card;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 获取指定高度下的卡片索引
|
|
272
|
+
Waterfallv2.prototype.updateCardsInView = async function ({ start, end, force = false }) {
|
|
273
|
+
const options = this.options;
|
|
274
|
+
const minHeight = this.getMinHeight();
|
|
275
|
+
const endBuffer = end + options.bufferHeight;
|
|
276
|
+
if (minHeight < endBuffer) {
|
|
277
|
+
// 如果不够 进行补建
|
|
278
|
+
await this.createCards({ end: endBuffer });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const startNum = start - options.bufferHeight;
|
|
282
|
+
const endNum = end + options.bufferHeight;
|
|
283
|
+
// 新方案:基于数据ID映射机制
|
|
284
|
+
const newActiveNodes = new Map();
|
|
285
|
+
for (let i = 0; i < this.columns; i++) {
|
|
286
|
+
const column = this.columnItems[i];
|
|
287
|
+
|
|
288
|
+
for (let j = 0; j < column.children.length; j++) {
|
|
289
|
+
const row = column.children[j];
|
|
290
|
+
const dataId = row.dataId; // 使用dataId替代renderIndex
|
|
291
|
+
|
|
292
|
+
// 验证数据ID有效性
|
|
293
|
+
if (!this.dataIdMap.has(dataId)) {
|
|
294
|
+
console.warn('Waterfallv2: Invalid dataId detected', dataId);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const dataInfo = this.dataIdMap.get(dataId);
|
|
299
|
+
|
|
300
|
+
if (!dataInfo) {
|
|
301
|
+
console.warn('Waterfallv2: Invalid data for dataId', dataId);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 通过 dataId 找到对应的数据对象
|
|
306
|
+
const data = options.data.find(item => item.dataId === dataId);
|
|
307
|
+
|
|
308
|
+
// 如果当前这个节点是特殊节点,只更新位置,不参与节点复用
|
|
309
|
+
let specialNode = false;
|
|
310
|
+
if (options.shouldOccupySpace) {
|
|
311
|
+
specialNode = options.shouldOccupySpace(data) || false;
|
|
312
|
+
}
|
|
313
|
+
if (specialNode) {
|
|
314
|
+
// 特殊节点:只更新位置,不参与复用逻辑
|
|
315
|
+
if (row.$node && row.$node.length) {
|
|
316
|
+
row.$node.css({
|
|
317
|
+
'transform': `translate(${row.left}px,${row.top}px)`,
|
|
318
|
+
}).attr('data-index', dataId);
|
|
319
|
+
|
|
320
|
+
if (force) {
|
|
321
|
+
this.updateRenderUI(row.$node, data, dataId);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
console.warn('Waterfallv2: Special node DOM not found for dataId', dataId);
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 在可视区域内 进行有关卡片的操作
|
|
330
|
+
const bool = row.top <= endNum && row.bottom >= startNum;
|
|
331
|
+
if (bool) {
|
|
332
|
+
const $card = this.activeNodes.get(dataId);
|
|
333
|
+
|
|
334
|
+
let $node = null;
|
|
335
|
+
|
|
336
|
+
let bool = true;
|
|
337
|
+
if ($card) {
|
|
338
|
+
bool = hasNodeInActives(newActiveNodes, $card)
|
|
339
|
+
}
|
|
340
|
+
if (bool === false) {
|
|
341
|
+
$node = $card;
|
|
342
|
+
this.activeNodes.delete(dataId);
|
|
343
|
+
if (force) {
|
|
344
|
+
this.updateRenderUI($node, data, dataId);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
} else {
|
|
348
|
+
const $card = this.allReadyNodes.get(dataId);
|
|
349
|
+
let bool = true;
|
|
350
|
+
if ($card) {
|
|
351
|
+
bool = hasNodeInActives(newActiveNodes, $card)
|
|
352
|
+
}
|
|
353
|
+
if (bool === false) {
|
|
354
|
+
$node = $card;
|
|
355
|
+
this.updateRenderUI($node, data, dataId);
|
|
356
|
+
|
|
357
|
+
} else {
|
|
358
|
+
$node = getNodePoolPop(this.nodePool, newActiveNodes);
|
|
359
|
+
if ($node === null) {
|
|
360
|
+
$node = this.appendCard(data, dataId, {
|
|
361
|
+
top: row.top, left: row.left
|
|
362
|
+
});
|
|
363
|
+
row.$node = $node;
|
|
364
|
+
} else {
|
|
365
|
+
this.updateRenderUI($node, data, dataId);
|
|
366
|
+
row.$node = $node;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
$node.css({
|
|
371
|
+
'transform': `translate(${row.left}px,${row.top}px)`,
|
|
372
|
+
}).attr('data-index', dataId);
|
|
373
|
+
|
|
374
|
+
newActiveNodes.set(dataId, $node);
|
|
375
|
+
|
|
376
|
+
// 清除掉在NodePool中的card
|
|
377
|
+
const index = this.nodePool.indexOf($card);
|
|
378
|
+
if (index !== -1) {
|
|
379
|
+
this.nodePool.splice(index, 1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 阶段2:处理不活跃节点
|
|
386
|
+
this.activeNodes.forEach($node => {
|
|
387
|
+
$node.css('transform', `translateY(-9999px)`);// 移出可视区域
|
|
388
|
+
if (this.nodePool.indexOf($node) === -1) {
|
|
389
|
+
this.nodePool.push($node);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
this.activeNodes = newActiveNodes;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
Waterfallv2.prototype.getMaxHeight = function () {
|
|
397
|
+
let maxHeight = 0;
|
|
398
|
+
for (let i = 0; i < this.columns; i++) {
|
|
399
|
+
const column = this.columnItems[i];
|
|
400
|
+
// 优先从 children 最后一个 row.bottom 读取,避免 column.bottom 因各种操作累积误差
|
|
401
|
+
if (column.children.length > 0) {
|
|
402
|
+
const lastRow = column.children[column.children.length - 1];
|
|
403
|
+
maxHeight = Math.max(maxHeight, lastRow.bottom);
|
|
404
|
+
} else {
|
|
405
|
+
maxHeight = Math.max(maxHeight, column.bottom);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return maxHeight;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
Waterfallv2.prototype.getMinHeight = function () {
|
|
412
|
+
let minHeight = 0;
|
|
413
|
+
for (let i = 0; i < this.columns; i++) {
|
|
414
|
+
const column = this.columnItems[i]
|
|
415
|
+
if (minHeight === 0) {
|
|
416
|
+
minHeight = column.bottom;
|
|
417
|
+
}
|
|
418
|
+
minHeight = Math.min(minHeight, column.bottom);
|
|
419
|
+
}
|
|
420
|
+
return minHeight;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
Waterfallv2.prototype.getMinHeightColumn = function () {
|
|
424
|
+
let minHeight = -1;
|
|
425
|
+
let mimHeightColumn = null;
|
|
426
|
+
let index = -1;
|
|
427
|
+
const startPoints = this.options.startPoints || [];
|
|
428
|
+
for (let i = 0; i < this.columns; i++) {
|
|
429
|
+
const column = this.columnItems[i]
|
|
430
|
+
if (minHeight === -1) {
|
|
431
|
+
minHeight = column.bottom;
|
|
432
|
+
mimHeightColumn = column;
|
|
433
|
+
index = i;
|
|
434
|
+
} else if (minHeight > column.bottom) {
|
|
435
|
+
minHeight = column.bottom;
|
|
436
|
+
mimHeightColumn = column;
|
|
437
|
+
index = i;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (minHeight === 0 && index > -1) {
|
|
442
|
+
if (startPoints.length > index) {
|
|
443
|
+
mimHeightColumn.bottom = startPoints[index];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return mimHeightColumn;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 创建卡片
|
|
451
|
+
Waterfallv2.prototype.createCards = function ({ end, dataId = -1 }, callback) {
|
|
452
|
+
const self = this;
|
|
453
|
+
const options = this.options;
|
|
454
|
+
|
|
455
|
+
return new Promise((resolve) => {
|
|
456
|
+
// 获取下一个未渲染的数据(按 options.data 顺序)
|
|
457
|
+
let nextDataId = null;
|
|
458
|
+
let nextData = null;
|
|
459
|
+
for (let i = 0; i < options.data.length; i++) {
|
|
460
|
+
const item = options.data[i];
|
|
461
|
+
const id = item.dataId;
|
|
462
|
+
if (!this.renderedDataIds.has(id)) {
|
|
463
|
+
nextDataId = id;
|
|
464
|
+
nextData = item;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 如果没有更多数据需要渲染
|
|
470
|
+
if (nextDataId === null) {
|
|
471
|
+
this.setScrollHeight();
|
|
472
|
+
return resolve();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!this.dataIdMap.has(nextDataId)) {
|
|
476
|
+
console.warn('Waterfallv2: Invalid dataId in createCards', nextDataId);
|
|
477
|
+
return resolve();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const data = nextData;
|
|
481
|
+
|
|
482
|
+
let column = this.getMinHeightColumn();
|
|
483
|
+
if (column === null) {
|
|
484
|
+
column = this.columnItems[0];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const top = column.bottom === 0 ? options.marginTop : (column.bottom + options.rowGap);
|
|
488
|
+
const position = { top, left: column.left };
|
|
489
|
+
const row = createDefaultRow(position);
|
|
490
|
+
|
|
491
|
+
this.renderIndex += 1;
|
|
492
|
+
|
|
493
|
+
let specialNode = false;
|
|
494
|
+
|
|
495
|
+
if (options.shouldOccupySpace) {
|
|
496
|
+
specialNode = options.shouldOccupySpace(data) || false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// 添加卡片,使用dataId作为唯一标识
|
|
500
|
+
let $card = null;
|
|
501
|
+
if (this.nodePool.length === 0 || specialNode === true) {
|
|
502
|
+
$card = this.appendCard(data, nextDataId, position);
|
|
503
|
+
} else {
|
|
504
|
+
const $tmp = getNodePoolPop(this.nodePool, this.activeNodes);
|
|
505
|
+
if ($tmp) {
|
|
506
|
+
$card = $tmp;
|
|
507
|
+
$card.css({
|
|
508
|
+
'transform': `translate(${row.left}px,${row.top}px)`,
|
|
509
|
+
}).attr('data-index', nextDataId);
|
|
510
|
+
this.updateRenderUI($card, data, nextDataId);
|
|
511
|
+
|
|
512
|
+
} else {
|
|
513
|
+
$card = this.appendCard(data, nextDataId, position);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
row.$node = $card;
|
|
519
|
+
row.dataId = nextDataId;
|
|
520
|
+
|
|
521
|
+
if (specialNode === false) {
|
|
522
|
+
this.activeNodes.set(nextDataId, $card);
|
|
523
|
+
this.allReadyNodes.set(nextDataId, $card);
|
|
524
|
+
} else {
|
|
525
|
+
this.allReadyNodes.set(nextDataId, null);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
this.renderedDataIds.add(nextDataId);
|
|
529
|
+
|
|
530
|
+
column.bottom = top + $card.height();
|
|
531
|
+
|
|
532
|
+
column.children.push(row);
|
|
533
|
+
row.bottom = column.bottom;
|
|
534
|
+
|
|
535
|
+
// 建立 dataIdMap -> row 的引用,用于快速访问布局信息
|
|
536
|
+
const dataInfo = this.dataIdMap.get(nextDataId);
|
|
537
|
+
if (dataInfo) {
|
|
538
|
+
dataInfo.layoutInfo = row;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 检查是否需要继续创建卡片
|
|
542
|
+
const minHeight = this.getMinHeight();
|
|
543
|
+
const hasMoreData = this.renderedDataIds.size < this.dataIdMap.size;
|
|
544
|
+
|
|
545
|
+
if (hasMoreData && (minHeight < end)) {
|
|
546
|
+
|
|
547
|
+
this.createCards({ end }, () => {
|
|
548
|
+
resolve();
|
|
549
|
+
if (callback) {
|
|
550
|
+
callback();
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
} else {
|
|
554
|
+
this.setScrollHeight();
|
|
555
|
+
resolve();
|
|
556
|
+
if (callback) {
|
|
557
|
+
callback();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
Waterfallv2.prototype.createColumn = function (index) {
|
|
566
|
+
const res = {
|
|
567
|
+
left: 0,
|
|
568
|
+
bottom: 0,
|
|
569
|
+
width: 0,
|
|
570
|
+
children: []
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const options = this.options;
|
|
574
|
+
const columnWidth = this.columnWidth;
|
|
575
|
+
|
|
576
|
+
res.width = this.columnWidth;
|
|
577
|
+
res.left = (index * (columnWidth + options.columnGap));
|
|
578
|
+
return res;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
Waterfallv2.prototype.updateRenderUI = function ($node, data, dataId) {
|
|
582
|
+
const options = this.options;
|
|
583
|
+
|
|
584
|
+
if (!this.dataIdMap.has(dataId)) {
|
|
585
|
+
console.error('Waterfallv2: Invalid dataId in updateRenderUI', dataId);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// 实时获取数据在 options.data 中的当前索引
|
|
590
|
+
const originalIndex = options.data.indexOf(data);
|
|
591
|
+
|
|
592
|
+
if (this.hasUpdateItem === true) {
|
|
593
|
+
options.updateItem($node, data, originalIndex);
|
|
594
|
+
} else {
|
|
595
|
+
const str = options.renderItem(data, originalIndex, $node);
|
|
596
|
+
if (str) {
|
|
597
|
+
$node.html(str);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// 辅助方法:使用 renderItem 渲染节点(用于全新节点)
|
|
603
|
+
Waterfallv2.prototype.renderUI = function ($node, data, dataId) {
|
|
604
|
+
const options = this.options;
|
|
605
|
+
|
|
606
|
+
if (!this.dataIdMap.has(dataId)) {
|
|
607
|
+
console.error('Waterfallv2: Invalid dataId in renderUI', dataId);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 实时获取数据在 options.data 中的当前索引
|
|
612
|
+
const originalIndex = options.data.indexOf(data);
|
|
613
|
+
const str = options.renderItem(data, originalIndex, $node);
|
|
614
|
+
if (str) {
|
|
615
|
+
$node.html(str);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
Waterfallv2.prototype.updateData = async function (newData) {
|
|
622
|
+
const options = this.options;
|
|
623
|
+
|
|
624
|
+
// 步骤0: 为新数据中没有 dataId 的元素分配新 dataId(视为新增元素)
|
|
625
|
+
// 注意:这里只给缺失 dataId 的元素分配,已有 dataId 的元素保持不变
|
|
626
|
+
newData.forEach(item => {
|
|
627
|
+
if (item.dataId === undefined || item.dataId === null) {
|
|
628
|
+
item.dataId = 'pix_wf_' + (_globalDataIdCounter++);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// 构建新数据的 dataId Set,用于快速判断
|
|
633
|
+
const newDataIdSet = new Set(newData.map(item => item.dataId));
|
|
634
|
+
// 构建新数据的 dataId -> 数据对象 Map,用于快速查找
|
|
635
|
+
const newDataMap = new Map(newData.map(item => [item.dataId, item]));
|
|
636
|
+
|
|
637
|
+
// 步骤1: 找出需要删除的 dataId(旧数据有、新数据没有)
|
|
638
|
+
const toDeleteIds = [];
|
|
639
|
+
for (const dataId of this.dataIdMap.keys()) {
|
|
640
|
+
if (!newDataIdSet.has(dataId)) {
|
|
641
|
+
toDeleteIds.push(dataId);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 步骤2: 找出需要更新的 dataId(新旧数据都有)
|
|
646
|
+
const toUpdateIds = [];
|
|
647
|
+
for (const item of newData) {
|
|
648
|
+
if (this.dataIdMap.has(item.dataId)) {
|
|
649
|
+
toUpdateIds.push(item.dataId);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 步骤3: 找出需要新增的 dataId(新数据有、旧数据没有)
|
|
654
|
+
const toAddItems = [];
|
|
655
|
+
for (const item of newData) {
|
|
656
|
+
if (!this.dataIdMap.has(item.dataId)) {
|
|
657
|
+
toAddItems.push(item);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// 步骤4: 先同步更新 options.data 为新数据
|
|
662
|
+
options.data = newData;
|
|
663
|
+
|
|
664
|
+
// 步骤5: 执行删除操作(复用 removeCard 的内部逻辑,但不触发视图刷新)
|
|
665
|
+
for (const dataId of toDeleteIds) {
|
|
666
|
+
this._removeCardInternal(dataId);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// 步骤6: 执行更新操作(批量获取新高度,应用高度变化)
|
|
670
|
+
if (toUpdateIds.length > 0) {
|
|
671
|
+
// 只对已渲染的卡片进行高度更新
|
|
672
|
+
const renderedUpdateIds = toUpdateIds.filter(id => this.renderedDataIds.has(id));
|
|
673
|
+
|
|
674
|
+
if (renderedUpdateIds.length > 0) {
|
|
675
|
+
const newHeightsMap = await this.getBatchCardNewHeights(renderedUpdateIds, newData);
|
|
676
|
+
|
|
677
|
+
// 收集高度变化
|
|
678
|
+
const heightChanges = new Map();
|
|
679
|
+
for (const dataId of renderedUpdateIds) {
|
|
680
|
+
const oldHeight = this.getCardOldHeight(dataId);
|
|
681
|
+
const newHeight = newHeightsMap.get(dataId);
|
|
682
|
+
if (oldHeight !== newHeight) {
|
|
683
|
+
heightChanges.set(dataId, {
|
|
684
|
+
oldHeight,
|
|
685
|
+
newHeight,
|
|
686
|
+
heightDiff: newHeight - oldHeight
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// 使用累积差分算法更新每一列的布局
|
|
692
|
+
for (let i = 0; i < this.columnItems.length; i++) {
|
|
693
|
+
const column = this.columnItems[i];
|
|
694
|
+
let accumulatedDiff = 0;
|
|
695
|
+
|
|
696
|
+
for (let j = 0; j < column.children.length; j++) {
|
|
697
|
+
const row = column.children[j];
|
|
698
|
+
const dataId = row.dataId;
|
|
699
|
+
|
|
700
|
+
if (accumulatedDiff !== 0) {
|
|
701
|
+
row.top += accumulatedDiff;
|
|
702
|
+
row.bottom += accumulatedDiff;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (heightChanges.has(dataId)) {
|
|
706
|
+
const change = heightChanges.get(dataId);
|
|
707
|
+
accumulatedDiff += change.heightDiff;
|
|
708
|
+
row.bottom += change.heightDiff;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (accumulatedDiff !== 0) {
|
|
713
|
+
column.bottom += accumulatedDiff;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// 步骤7: 执行新增操作(为新增数据建立 dataIdMap 映射,等待 updateVisibleItems 时自动渲染)
|
|
720
|
+
for (const item of toAddItems) {
|
|
721
|
+
const dataId = item.dataId;
|
|
722
|
+
if (!this.dataIdMap.has(dataId)) {
|
|
723
|
+
this.dataIdMap.set(dataId, {
|
|
724
|
+
data: true,
|
|
725
|
+
layoutInfo: null
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// 步骤8: 同步视图
|
|
731
|
+
// 修正 scrollTop:删除数据后,若当前滚动位置超过新的内容高度,则重置到顶部
|
|
732
|
+
// 否则 updateVisibleItems 会因为 scrollTop 过大而判断所有剩余卡片不在可视区域,导致位置不更新
|
|
733
|
+
const newMaxHeight = this.getMaxHeight();
|
|
734
|
+
if (this.scrollTop > newMaxHeight) {
|
|
735
|
+
this.scrollTop = 0;
|
|
736
|
+
const $scrollDom = options.scrollDom ? $(options.scrollDom) : $(options.container);
|
|
737
|
+
$scrollDom.scrollTop(0);
|
|
738
|
+
}
|
|
739
|
+
this.setScrollHeight();
|
|
740
|
+
this.updateVisibleItems(true);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* 删除卡片的内部实现(不触发视图刷新,供 updateData 批量调用)
|
|
745
|
+
* @private
|
|
746
|
+
*/
|
|
747
|
+
Waterfallv2.prototype._removeCardInternal = function (dataId) {
|
|
748
|
+
const options = this.options;
|
|
749
|
+
|
|
750
|
+
if (!this.dataIdMap.has(dataId)) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// 找到该卡片所在列及其 row,计算被删除卡片的高度
|
|
755
|
+
let targetColumn = null;
|
|
756
|
+
let targetRowIndex = -1;
|
|
757
|
+
let removedHeight = 0;
|
|
758
|
+
|
|
759
|
+
for (let i = 0; i < this.columnItems.length; i++) {
|
|
760
|
+
const column = this.columnItems[i];
|
|
761
|
+
for (let j = 0; j < column.children.length; j++) {
|
|
762
|
+
const row = column.children[j];
|
|
763
|
+
if (row.dataId === dataId) {
|
|
764
|
+
targetColumn = column;
|
|
765
|
+
targetRowIndex = j;
|
|
766
|
+
removedHeight = row.bottom - row.top;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (targetColumn !== null) break;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (targetColumn === null) {
|
|
774
|
+
// 卡片尚未渲染到布局中,只清理数据映射即可
|
|
775
|
+
this._cleanupCardData(dataId);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// 回收被删除卡片的 DOM 节点
|
|
780
|
+
// 优先从 activeNodes 获取节点引用(虚拟化后 row.$node 可能已被复用给其他卡片)
|
|
781
|
+
const isSpecial = this.allReadyNodes.get(dataId) === null;
|
|
782
|
+
const $activeNode = this.activeNodes.get(dataId);
|
|
783
|
+
|
|
784
|
+
if ($activeNode && $activeNode.length) {
|
|
785
|
+
// 节点当前在可视区域,直接回收
|
|
786
|
+
$activeNode.css('transform', 'translateY(-9999px)');
|
|
787
|
+
if (!isSpecial) {
|
|
788
|
+
if (this.nodePool.indexOf($activeNode) === -1) {
|
|
789
|
+
this.nodePool.push($activeNode);
|
|
790
|
+
}
|
|
791
|
+
} else {
|
|
792
|
+
$activeNode.remove();
|
|
793
|
+
}
|
|
794
|
+
} else if (isSpecial) {
|
|
795
|
+
// 特殊节点不在 activeNodes 里,从 row.$node 获取并移除
|
|
796
|
+
const removedRow = targetColumn.children[targetRowIndex];
|
|
797
|
+
const $specialNode = removedRow.$node;
|
|
798
|
+
if ($specialNode && $specialNode.length) {
|
|
799
|
+
$specialNode.remove();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// 普通节点不在 activeNodes 里,说明已被回收到 nodePool,无需额外处理
|
|
803
|
+
|
|
804
|
+
// 从该列的 children 中移除该 row
|
|
805
|
+
targetColumn.children.splice(targetRowIndex, 1);
|
|
806
|
+
|
|
807
|
+
// 对该列中被删除卡片之后的所有 row,向上位移
|
|
808
|
+
// 后面还有卡片时:位移量 = removedHeight + rowGap(卡片高度 + 与下一张卡片的间距)
|
|
809
|
+
// 后面没有卡片时:位移量 = removedHeight(仅减去卡片高度,无需减 rowGap)
|
|
810
|
+
const hasFollowingCards = targetRowIndex < targetColumn.children.length;
|
|
811
|
+
const shiftAmount = hasFollowingCards ? (removedHeight + options.rowGap) : removedHeight;
|
|
812
|
+
for (let j = targetRowIndex; j < targetColumn.children.length; j++) {
|
|
813
|
+
const row = targetColumn.children[j];
|
|
814
|
+
row.top -= shiftAmount;
|
|
815
|
+
row.bottom -= shiftAmount;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// 更新该列的 bottom
|
|
819
|
+
if (targetColumn.children.length === 0) {
|
|
820
|
+
targetColumn.bottom = 0;
|
|
821
|
+
} else {
|
|
822
|
+
targetColumn.bottom -= shiftAmount;
|
|
823
|
+
if (targetColumn.bottom < 0) targetColumn.bottom = 0;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// 清理各种映射中的引用
|
|
827
|
+
this._cleanupCardData(dataId);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// 辅助方法:获取卡片的旧高度(从布局信息)
|
|
831
|
+
Waterfallv2.prototype.getCardOldHeight = function (dataId) {
|
|
832
|
+
const dataInfo = this.dataIdMap.get(dataId);
|
|
833
|
+
|
|
834
|
+
if (!dataInfo) {
|
|
835
|
+
return 0;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (!dataInfo.layoutInfo) {
|
|
839
|
+
return 0;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const row = dataInfo.layoutInfo;
|
|
843
|
+
return row.bottom - row.top;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// 辅助方法:获取特殊节点的 DOM 引用
|
|
847
|
+
Waterfallv2.prototype.getSpecialNodeDOM = function (dataId) {
|
|
848
|
+
const dataInfo = this.dataIdMap.get(dataId);
|
|
849
|
+
if (dataInfo && dataInfo.layoutInfo && dataInfo.layoutInfo.$node) {
|
|
850
|
+
return dataInfo.layoutInfo.$node;
|
|
851
|
+
}
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// 辅助方法:批量获取卡片的新高度(用于 updateData)
|
|
856
|
+
Waterfallv2.prototype.getBatchCardNewHeights = async function (dataIds, newData) {
|
|
857
|
+
const options = this.options;
|
|
858
|
+
const $ = getEnv().$;
|
|
859
|
+
const $container = $(options.container);
|
|
860
|
+
const $viewport = $container.find('.Waterfallv2-list-viewport');
|
|
861
|
+
|
|
862
|
+
const cardInfos = new Map();
|
|
863
|
+
|
|
864
|
+
const borrowedNodes = [];
|
|
865
|
+
const tempNodesToDelete = [];
|
|
866
|
+
|
|
867
|
+
// 构建 dataId -> 数据对象 的快速查找 Map,避免多次 find 遍历
|
|
868
|
+
const newDataMap = new Map();
|
|
869
|
+
for (const item of newData) {
|
|
870
|
+
if (item.dataId != null) {
|
|
871
|
+
newDataMap.set(item.dataId, item);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// 第一步:优先处理所有可视区域节点(activeNodes)
|
|
876
|
+
for (let dataId of dataIds) {
|
|
877
|
+
if (this.activeNodes.has(dataId)) {
|
|
878
|
+
const $node = this.activeNodes.get(dataId);
|
|
879
|
+
// 通过 dataId 找到对应的数据对象
|
|
880
|
+
const data = newDataMap.get(dataId);
|
|
881
|
+
|
|
882
|
+
if (!data) {
|
|
883
|
+
// 新数据中找不到对应 dataId,跳过更新,保留旧高度
|
|
884
|
+
console.warn('Waterfallv2: getBatchCardNewHeights - dataId not found in newData:', dataId);
|
|
885
|
+
cardInfos.set(dataId, { $node: $node, needCleanup: false, cleanupType: null });
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
this.updateRenderUI($node, data, dataId);
|
|
890
|
+
|
|
891
|
+
cardInfos.set(dataId, {
|
|
892
|
+
$node: $node,
|
|
893
|
+
needCleanup: false,
|
|
894
|
+
cleanupType: null
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// 第二步:处理所有非可视区域节点
|
|
900
|
+
for (let dataId of dataIds) {
|
|
901
|
+
if (this.activeNodes.has(dataId)) {
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// 通过 dataId 找到对应的数据对象
|
|
906
|
+
const data = newDataMap.get(dataId);
|
|
907
|
+
|
|
908
|
+
if (!data) {
|
|
909
|
+
// 新数据中找不到对应 dataId,跳过更新,保留旧高度
|
|
910
|
+
console.warn('Waterfallv2: getBatchCardNewHeights - dataId not found in newData:', dataId);
|
|
911
|
+
cardInfos.set(dataId, { $node: null, needCleanup: false, cleanupType: 'notfound' });
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (this.allReadyNodes.has(dataId)) {
|
|
916
|
+
const $existingNode = this.allReadyNodes.get(dataId);
|
|
917
|
+
|
|
918
|
+
if ($existingNode === null) {
|
|
919
|
+
const $specialNode = this.getSpecialNodeDOM(dataId);
|
|
920
|
+
|
|
921
|
+
if ($specialNode) {
|
|
922
|
+
this.updateRenderUI($specialNode, data, dataId);
|
|
923
|
+
|
|
924
|
+
cardInfos.set(dataId, {
|
|
925
|
+
$node: $specialNode,
|
|
926
|
+
needCleanup: false,
|
|
927
|
+
cleanupType: 'special'
|
|
928
|
+
});
|
|
929
|
+
} else {
|
|
930
|
+
console.warn('Waterfallv2: Special node DOM not found for dataId', dataId);
|
|
931
|
+
cardInfos.set(dataId, {
|
|
932
|
+
$node: null,
|
|
933
|
+
needCleanup: false,
|
|
934
|
+
cleanupType: 'special-notfound'
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
let $node = getNodePoolPop(this.nodePool, this.activeNodes);
|
|
941
|
+
|
|
942
|
+
if ($node) {
|
|
943
|
+
borrowedNodes.push($node);
|
|
944
|
+
|
|
945
|
+
$node.css({
|
|
946
|
+
'transform': 'translate(-9999px, -9999px)',
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
if (options.columns !== 1) {
|
|
950
|
+
$node.width(this.columnWidth + 'px');
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
$node.attr('data-index', dataId);
|
|
954
|
+
|
|
955
|
+
this.updateRenderUI($node, data, dataId);
|
|
956
|
+
|
|
957
|
+
cardInfos.set(dataId, {
|
|
958
|
+
$node: $node,
|
|
959
|
+
needCleanup: false,
|
|
960
|
+
cleanupType: 'borrowed'
|
|
961
|
+
});
|
|
962
|
+
} else {
|
|
963
|
+
$node = $(
|
|
964
|
+
`<div class="waterfallv2-item"
|
|
965
|
+
data-index="${dataId}"
|
|
966
|
+
style="position: absolute; transform: translate(-9999px, -9999px);"
|
|
967
|
+
>
|
|
968
|
+
</div>`
|
|
969
|
+
);
|
|
970
|
+
$viewport.append($node);
|
|
971
|
+
tempNodesToDelete.push($node);
|
|
972
|
+
|
|
973
|
+
if (options.columns !== 1) {
|
|
974
|
+
$node.width(this.columnWidth + 'px');
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
this.renderUI($node, data, dataId);
|
|
978
|
+
|
|
979
|
+
cardInfos.set(dataId, {
|
|
980
|
+
$node: $node,
|
|
981
|
+
needCleanup: false,
|
|
982
|
+
cleanupType: 'temp'
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
cardInfos.set(dataId, {
|
|
989
|
+
$node: null,
|
|
990
|
+
needCleanup: false,
|
|
991
|
+
cleanupType: 'notfound'
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// 第三步:在单个 requestAnimationFrame 中统一获取所有高度
|
|
996
|
+
return new Promise(resolve => {
|
|
997
|
+
requestAnimationFrame(() => {
|
|
998
|
+
const heightMap = new Map();
|
|
999
|
+
|
|
1000
|
+
for (let [dataId, info] of cardInfos) {
|
|
1001
|
+
if (info.$node) {
|
|
1002
|
+
heightMap.set(dataId, info.$node.height());
|
|
1003
|
+
} else {
|
|
1004
|
+
heightMap.set(dataId, this.getCardOldHeight(dataId));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// 清理:归还借用的节点到节点池
|
|
1009
|
+
for (let $node of borrowedNodes) {
|
|
1010
|
+
$node.css('transform', 'translateY(-9999px)');
|
|
1011
|
+
this.nodePool.push($node);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// 清理:删除临时节点
|
|
1015
|
+
for (let $node of tempNodesToDelete) {
|
|
1016
|
+
$node.remove();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
resolve(heightMap);
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// 辅助方法:获取卡片的新高度(通过渲染)- 用于单卡片更新
|
|
1025
|
+
Waterfallv2.prototype.getCardNewHeight = async function (dataId, data) {
|
|
1026
|
+
const options = this.options;
|
|
1027
|
+
const $ = getEnv().$;
|
|
1028
|
+
|
|
1029
|
+
// 情况1: 卡片在可视区域(activeNodes)
|
|
1030
|
+
if (this.activeNodes.has(dataId)) {
|
|
1031
|
+
const $node = this.activeNodes.get(dataId);
|
|
1032
|
+
|
|
1033
|
+
this.updateRenderUI($node, data, dataId);
|
|
1034
|
+
|
|
1035
|
+
return new Promise(resolve => {
|
|
1036
|
+
window.requestAnimationFrame(() => {
|
|
1037
|
+
resolve($node.height());
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// 情况2: 卡片不在可视区域(allReadyNodes)
|
|
1043
|
+
if (this.allReadyNodes.has(dataId)) {
|
|
1044
|
+
const $existingNode = this.allReadyNodes.get(dataId);
|
|
1045
|
+
|
|
1046
|
+
if ($existingNode === null) {
|
|
1047
|
+
const $specialNode = this.getSpecialNodeDOM(dataId);
|
|
1048
|
+
|
|
1049
|
+
if ($specialNode) {
|
|
1050
|
+
this.updateRenderUI($specialNode, data, dataId);
|
|
1051
|
+
|
|
1052
|
+
return new Promise(resolve => {
|
|
1053
|
+
window.requestAnimationFrame(() => {
|
|
1054
|
+
resolve($specialNode.height());
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
console.warn('Waterfallv2: Special node DOM not found for dataId', dataId);
|
|
1060
|
+
return this.getCardOldHeight(dataId);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const $container = $(options.container);
|
|
1064
|
+
const $viewport = $container.find('.Waterfallv2-list-viewport');
|
|
1065
|
+
|
|
1066
|
+
let $node = getNodePoolPop(this.nodePool, this.activeNodes);
|
|
1067
|
+
let isBorrowed = false;
|
|
1068
|
+
|
|
1069
|
+
if ($node) {
|
|
1070
|
+
isBorrowed = true;
|
|
1071
|
+
|
|
1072
|
+
$node.css({
|
|
1073
|
+
'transform': 'translate(-9999px, -9999px)',
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
$node.attr('data-index', dataId);
|
|
1077
|
+
|
|
1078
|
+
if (options.columns !== 1) {
|
|
1079
|
+
$node.width(this.columnWidth + 'px');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
this.updateRenderUI($node, data, dataId);
|
|
1083
|
+
} else {
|
|
1084
|
+
$node = $(
|
|
1085
|
+
`<div class="waterfallv2-item"
|
|
1086
|
+
data-index="${dataId}"
|
|
1087
|
+
style="position: absolute; transform: translate(-9999px, -9999px);"
|
|
1088
|
+
>
|
|
1089
|
+
</div>`
|
|
1090
|
+
);
|
|
1091
|
+
$viewport.append($node);
|
|
1092
|
+
|
|
1093
|
+
if (options.columns !== 1) {
|
|
1094
|
+
$node.width(this.columnWidth + 'px');
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
this.renderUI($node, data, dataId);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return new Promise(resolve => {
|
|
1101
|
+
window.requestAnimationFrame(() => {
|
|
1102
|
+
const newHeight = $node.height();
|
|
1103
|
+
|
|
1104
|
+
if (isBorrowed) {
|
|
1105
|
+
$node.css('transform', 'translateY(-9999px)');
|
|
1106
|
+
this.nodePool.push($node);
|
|
1107
|
+
} else {
|
|
1108
|
+
$node.remove();
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
resolve(newHeight);
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// 情况3: 未找到节点,返回旧高度
|
|
1117
|
+
return this.getCardOldHeight(dataId);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// 某个数据进行了UI变更,触发高度重新绘制
|
|
1121
|
+
/**
|
|
1122
|
+
* @param {string} dataId - 要更新的卡片的 dataId
|
|
1123
|
+
* @param {object} data - 更新后的数据对象
|
|
1124
|
+
*/
|
|
1125
|
+
Waterfallv2.prototype.updateCard = async function (dataId, data) {
|
|
1126
|
+
// 1. 校验参数
|
|
1127
|
+
if (typeof dataId !== 'string') {
|
|
1128
|
+
console.warn('Waterfallv2: updateCard - dataId must be a string');
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (!this.renderedDataIds.has(dataId)) {
|
|
1133
|
+
console.log('Waterfallv2: dataId not rendered yet', dataId);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// 2. 同步更新 options.data 中对应的数据
|
|
1138
|
+
const options = this.options;
|
|
1139
|
+
if (data !== undefined) {
|
|
1140
|
+
const index = options.data.findIndex(item => item.dataId === dataId);
|
|
1141
|
+
if (index !== -1) {
|
|
1142
|
+
options.data[index] = Object.assign({}, options.data[index], data);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const itemData = options.data.find(item => item.dataId === dataId);
|
|
1147
|
+
|
|
1148
|
+
const oldHeight = this.getCardOldHeight(dataId);
|
|
1149
|
+
|
|
1150
|
+
const newHeight = await this.getCardNewHeight(dataId, itemData);
|
|
1151
|
+
|
|
1152
|
+
this.applyHeightChange(dataId, oldHeight, newHeight);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// 应用高度变化到布局
|
|
1156
|
+
Waterfallv2.prototype.applyHeightChange = function (dataId, oldHeight, newHeight) {
|
|
1157
|
+
const minus = newHeight - oldHeight;
|
|
1158
|
+
|
|
1159
|
+
if (minus === 0) {
|
|
1160
|
+
console.log('Waterfallv2: Card height unchanged for dataId', dataId);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
console.log('Waterfallv2: Card height changed for dataId', dataId, 'from', oldHeight, 'to', newHeight, 'diff', minus);
|
|
1165
|
+
|
|
1166
|
+
let needUpdate = false;
|
|
1167
|
+
const columnItems = this.columnItems;
|
|
1168
|
+
|
|
1169
|
+
for (let i = 0; i < columnItems.length; i++) {
|
|
1170
|
+
const column = columnItems[i];
|
|
1171
|
+
let foundCard = false;
|
|
1172
|
+
|
|
1173
|
+
for (let j = 0; j < column.children.length; j++) {
|
|
1174
|
+
const row = column.children[j];
|
|
1175
|
+
|
|
1176
|
+
if (row.dataId === dataId) {
|
|
1177
|
+
foundCard = true;
|
|
1178
|
+
needUpdate = true;
|
|
1179
|
+
|
|
1180
|
+
row.bottom += minus;
|
|
1181
|
+
|
|
1182
|
+
} else if (foundCard) {
|
|
1183
|
+
row.top += minus;
|
|
1184
|
+
row.bottom += minus;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (foundCard) {
|
|
1189
|
+
column.bottom += minus;
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (needUpdate) {
|
|
1195
|
+
this.setScrollHeight();
|
|
1196
|
+
this.updateVisibleItems(true);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* 删除某个卡片,并动态重新位移被删除卡片所在列的其他卡片位置
|
|
1202
|
+
* @param {string} dataId - 要删除的卡片的 dataId
|
|
1203
|
+
*/
|
|
1204
|
+
Waterfallv2.prototype.removeCard = function (dataId) {
|
|
1205
|
+
const options = this.options;
|
|
1206
|
+
|
|
1207
|
+
// 1. 校验参数
|
|
1208
|
+
if (typeof dataId !== 'string') {
|
|
1209
|
+
console.warn('Waterfallv2: removeCard - dataId must be a string');
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// 2. 通过 dataId 查找对应的数据对象
|
|
1214
|
+
const targetData = options.data.find(item => item.dataId === dataId) || null;
|
|
1215
|
+
|
|
1216
|
+
if (!this.dataIdMap.has(dataId)) {
|
|
1217
|
+
console.warn('Waterfallv2: removeCard - dataId not in dataIdMap', dataId);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// 3. 从 options.data 中移除该数据(通过对象引用精确定位)
|
|
1222
|
+
const spliceIndex = options.data.indexOf(targetData);
|
|
1223
|
+
if (spliceIndex !== -1) {
|
|
1224
|
+
options.data.splice(spliceIndex, 1);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// 3. 找到该卡片所在列及其 row,计算被删除卡片的高度
|
|
1228
|
+
let targetColumn = null;
|
|
1229
|
+
let targetRowIndex = -1;
|
|
1230
|
+
let removedHeight = 0;
|
|
1231
|
+
|
|
1232
|
+
for (let i = 0; i < this.columnItems.length; i++) {
|
|
1233
|
+
const column = this.columnItems[i];
|
|
1234
|
+
for (let j = 0; j < column.children.length; j++) {
|
|
1235
|
+
const row = column.children[j];
|
|
1236
|
+
if (row.dataId === dataId) {
|
|
1237
|
+
targetColumn = column;
|
|
1238
|
+
targetRowIndex = j;
|
|
1239
|
+
removedHeight = row.bottom - row.top;
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (targetColumn !== null) break;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (targetColumn === null) {
|
|
1247
|
+
// 卡片尚未渲染到布局中,只清理数据映射即可
|
|
1248
|
+
console.log('Waterfallv2: removeCard - card not yet laid out, cleaning up data only', dataId);
|
|
1249
|
+
this._cleanupCardData(dataId);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// 4. 回收被删除卡片的 DOM 节点到节点池
|
|
1254
|
+
const removedRow = targetColumn.children[targetRowIndex];
|
|
1255
|
+
const $removedNode = removedRow.$node;
|
|
1256
|
+
|
|
1257
|
+
if ($removedNode && $removedNode.length) {
|
|
1258
|
+
// 移出可视区
|
|
1259
|
+
$removedNode.css('transform', 'translateY(-9999px)');
|
|
1260
|
+
// 放入节点池(如果不是特殊节点)
|
|
1261
|
+
const isSpecial = this.allReadyNodes.get(dataId) === null;
|
|
1262
|
+
if (!isSpecial) {
|
|
1263
|
+
if (this.nodePool.indexOf($removedNode) === -1) {
|
|
1264
|
+
this.nodePool.push($removedNode);
|
|
1265
|
+
}
|
|
1266
|
+
} else {
|
|
1267
|
+
// 特殊节点直接从 DOM 中移除
|
|
1268
|
+
$removedNode.remove();
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// 5. 从该列的 children 中移除该 row
|
|
1273
|
+
targetColumn.children.splice(targetRowIndex, 1);
|
|
1274
|
+
|
|
1275
|
+
// 6. 对该列中被删除卡片之后的所有 row,向上位移
|
|
1276
|
+
// 后面还有卡片时:位移量 = removedHeight + rowGap(卡片高度 + 与下一张卡片的间距)
|
|
1277
|
+
// 后面没有卡片时:位移量 = removedHeight(仅减去卡片高度,无需减 rowGap)
|
|
1278
|
+
const hasFollowingCards = targetRowIndex < targetColumn.children.length;
|
|
1279
|
+
const shiftAmount = hasFollowingCards ? (removedHeight + options.rowGap) : removedHeight;
|
|
1280
|
+
|
|
1281
|
+
for (let j = targetRowIndex; j < targetColumn.children.length; j++) {
|
|
1282
|
+
const row = targetColumn.children[j];
|
|
1283
|
+
row.top -= shiftAmount;
|
|
1284
|
+
row.bottom -= shiftAmount;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// 7. 更新该列的 bottom
|
|
1288
|
+
if (targetColumn.children.length === 0) {
|
|
1289
|
+
targetColumn.bottom = 0;
|
|
1290
|
+
} else {
|
|
1291
|
+
targetColumn.bottom -= shiftAmount;
|
|
1292
|
+
// 确保 bottom 不小于 0
|
|
1293
|
+
if (targetColumn.bottom < 0) targetColumn.bottom = 0;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// 8. 清理各种映射中的引用
|
|
1297
|
+
this._cleanupCardData(dataId);
|
|
1298
|
+
|
|
1299
|
+
// 9. 刷新视图(dataId 是稳定的唯一标识,无需重建映射)
|
|
1300
|
+
this.setScrollHeight();
|
|
1301
|
+
this.updateVisibleItems(true);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* 清理被删除卡片在各映射中的引用
|
|
1306
|
+
* @private
|
|
1307
|
+
*/
|
|
1308
|
+
Waterfallv2.prototype._cleanupCardData = function (dataId) {
|
|
1309
|
+
// 从 activeNodes 中移除
|
|
1310
|
+
this.activeNodes.delete(dataId);
|
|
1311
|
+
|
|
1312
|
+
// 从 allReadyNodes 中移除
|
|
1313
|
+
this.allReadyNodes.delete(dataId);
|
|
1314
|
+
|
|
1315
|
+
// 从 renderedDataIds 中移除
|
|
1316
|
+
this.renderedDataIds.delete(dataId);
|
|
1317
|
+
|
|
1318
|
+
// 从 dataIdMap 中移除
|
|
1319
|
+
this.dataIdMap.delete(dataId);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// _rebuildDataIdMap 已不再需要:
|
|
1323
|
+
// dataId 现在是数据元素自身携带的稳定唯一标识,删除某条数据后其他数据的 dataId 不变,无需重建映射。
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
// 展示loading的回调函数
|
|
1327
|
+
Waterfallv2.prototype.showLoading = function (callback = null) {
|
|
1328
|
+
this.isShowLoading = true;
|
|
1329
|
+
const options = this.options;
|
|
1330
|
+
let $node = null
|
|
1331
|
+
if (this.$loadingNode) {
|
|
1332
|
+
let loadingTop = this.getMaxHeight() + options.rowGap
|
|
1333
|
+
this.$loadingNode.css('transform', `translate(0px,${loadingTop}px)`);
|
|
1334
|
+
$node = this.$loadingNode;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (callback) callback($node)
|
|
1338
|
+
// 延迟到下一帧再计算高度,确保 loadingNode 已完成渲染,height() 返回准确值
|
|
1339
|
+
window.requestAnimationFrame(() => {
|
|
1340
|
+
this.setScrollHeight();
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// 隐藏loading的回调函数
|
|
1345
|
+
Waterfallv2.prototype.hideLoading = function (callback = null) {
|
|
1346
|
+
this.isShowLoading = false;
|
|
1347
|
+
const options = this.options;
|
|
1348
|
+
let $node = null
|
|
1349
|
+
if (this.$loadingNode) {
|
|
1350
|
+
this.$loadingNode.css('transform', `translate(0px,-99999px)`);
|
|
1351
|
+
$node = this.$loadingNode
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (callback) callback($node)
|
|
1355
|
+
// 延迟到下一帧再计算高度,确保 loadingNode 已完成位移渲染
|
|
1356
|
+
window.requestAnimationFrame(() => {
|
|
1357
|
+
this.setScrollHeight();
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// 设置滚动条列表的高度
|
|
1362
|
+
Waterfallv2.prototype.setScrollHeight = function () {
|
|
1363
|
+
const options = this.options;
|
|
1364
|
+
const $container = $(options.container);
|
|
1365
|
+
let h = this.getMaxHeight();
|
|
1366
|
+
if (this.isShowLoading === true) {
|
|
1367
|
+
if (this.$loadingNode) {
|
|
1368
|
+
h += options.rowGap + this.$loadingNode.height();
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
h += options.marginBottom;
|
|
1372
|
+
$container.find('.waterfallv2-list-scroll').css('height', h + 'px');
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// 销毁自己
|
|
1376
|
+
Waterfallv2.prototype.destroy = function () {
|
|
1377
|
+
const options = this.options;
|
|
1378
|
+
const $container = $(options.container);
|
|
1379
|
+
$container.html('');
|
|
1380
|
+
this.activeNodes.clear();
|
|
1381
|
+
this.allReadyNodes.clear();
|
|
1382
|
+
this.dataIdMap.clear();
|
|
1383
|
+
this.renderedDataIds.clear();
|
|
1384
|
+
this.nodePool = [];
|
|
1385
|
+
this.columnItems = [];
|
|
1386
|
+
this.columns = 0;
|
|
1387
|
+
this.columnWidth = 0;
|
|
1388
|
+
this.renderIndex = 0;
|
|
1389
|
+
this.scrollTop = 0;
|
|
1390
|
+
this.isShowLoading = false;
|
|
1391
|
+
this.$loadingNode = null;
|
|
1392
|
+
this.$scrollDom = null;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function createDefaultRow({ top, left }) {
|
|
1396
|
+
return {
|
|
1397
|
+
top,
|
|
1398
|
+
left,
|
|
1399
|
+
bottom: 0,
|
|
1400
|
+
$node: null,
|
|
1401
|
+
dataId: -1 // 新方案:使用dataId替代renderIndex
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
function getBottomByColumn(column) {
|
|
1407
|
+
if (column.children.length === 0) {
|
|
1408
|
+
return 0;
|
|
1409
|
+
}
|
|
1410
|
+
const child = column.children;
|
|
1411
|
+
return child[child.length - 1].bottom;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function hasNodeInActives(mapObj, $node) {
|
|
1415
|
+
let bool = false
|
|
1416
|
+
mapObj.forEach((item) => {
|
|
1417
|
+
if (item === $node) {
|
|
1418
|
+
bool = true;
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
return bool;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// 从众多jq对象数组取得一个不重复的
|
|
1425
|
+
function getNodePoolPop(nodePool, actives) {
|
|
1426
|
+
for (let i = 0; i < nodePool.length; i++) {
|
|
1427
|
+
const $node = nodePool[i];
|
|
1428
|
+
if (!hasNodeInActives(actives, $node)) {
|
|
1429
|
+
nodePool.splice(i, 1);
|
|
1430
|
+
return $node;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|